From b729794e8800b587a198100dfd3e2bce528d3833 Mon Sep 17 00:00:00 2001 From: Sergey Sklarovs Date: Mon, 21 Jun 2021 22:29:40 +0200 Subject: [PATCH 01/14] Add support for Bitbucket Cloud and Server --- CHANGELOG.md | 4 + README.md | 61 +++++ build.gradle | 1 + .../com/diffplug/blowdryer/Blowdryer.java | 11 +- .../diffplug/blowdryer/BlowdryerSetup.java | 233 +++++++++++++++++- .../blowdryer/BlowdryerPluginAuthTest.java | 47 +++- .../blowdryer/BlowdryerPluginTest.java | 83 +++++++ .../com/diffplug/blowdryer/BlowdryerTest.java | 169 ++++++++++++- .../com/diffplug/blowdryer/GradleHarness.java | 5 + 9 files changed, 596 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e04c0d..7a91519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `filenameSafe` now removes query parameters and appends file name to the end of the link to preserve its original file extension. +- Support for Bitbucket Cloud and Server ([#20](add-link-to-PR)). + ## [1.2.1] - 2021-06-01 ### Fixed - `repoSubfolder` doesn't do anything in `localJar` mode, so setting `repoSubfolder` ought to be an error, and now it is ([#22](https://github.com/diffplug/blowdryer/pull/22)). diff --git a/README.md b/README.md index d98c9a1..80b5df1 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,67 @@ somePlugin { `Blowdryer.prop()` parses a java `.properties` file which was downloaded using `Blowdryer.file()`, and then returns the value associated with the given key. +### Bitbucket support +#### Bitbucket Cloud + +For public Bitbucket Cloud repo, use such configuration: +```gradle +plugins { + id 'com.diffplug.blowdryerSetup' version '1.2.2' +} + +blowdryerSetup { + bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5') + // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' + // or 'branch', 'feature/branch' +} +``` + +For private Bitbucket Cloud repo, use such configuration: +```gradle +plugins { + id 'com.diffplug.blowdryerSetup' version '1.2.2' +} + +blowdryerSetup { + bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').cloudAuth("username:appPassword") + // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' + // or 'branch', 'feature/branch' +} +``` +Reference on how to create [application password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/). + +#### Bitbucket Server + +For public Bitbucket Server repo, use such configuration: +```gradle +plugins { + id 'com.diffplug.blowdryerSetup' version '1.2.2' +} + +blowdryerSetup { + bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').server() + // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').server().customDomainHttps('my.bitbucket.company.domain.com') + // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' + // or 'branch', 'feature/branch' +} +``` + +For private Bitbucket Server repo, use such configuration: +```gradle +plugins { + id 'com.diffplug.blowdryerSetup' version '1.2.2' +} + +blowdryerSetup { + bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').serverAuth('personalAccessToken') + // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').serverAuth('personalAccessToken').customDomainHttps('my.bitbucket.company.domain.com') + // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' + // or 'branch', 'feature/branch' +} +``` +Reference on how to create [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). + ### Chinese for "dry" (干) If you like brevity and unicode, you can replace `Blowdryer` with `干`. We'll use `干` throughout the rest of the readme, but you can find-replace `干` with `Blowdryer` and get the same results. diff --git a/build.gradle b/build.gradle index bfd1863..67234ee 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ apply from: 干.file('spotless/java.gradle') dependencies { implementation 'com.squareup.okhttp3:okhttp:4.2.2' implementation 'com.squareup.okio:okio:2.4.1' + implementation 'com.google.code.gson:gson:2.8.7' implementation 'com.diffplug.durian:durian-core:1.2.0' implementation 'com.diffplug.durian:durian-io:1.2.0' testImplementation 'junit:junit:4.12' diff --git a/src/main/java/com/diffplug/blowdryer/Blowdryer.java b/src/main/java/com/diffplug/blowdryer/Blowdryer.java index 1bdc949..5f08398 100644 --- a/src/main/java/com/diffplug/blowdryer/Blowdryer.java +++ b/src/main/java/com/diffplug/blowdryer/Blowdryer.java @@ -47,6 +47,8 @@ import okio.Okio; import org.gradle.api.Project; +import static com.diffplug.blowdryer.BlowdryerSetup.BITBUCKET_COMMIT_HASH_CACHE; + /** * Public static methods which retrieve resources as * determined by {@link BlowdryerSetup}. @@ -72,6 +74,7 @@ static void wipeEntireCache() { try { urlToContent.clear(); fileToProps.clear(); + BITBUCKET_COMMIT_HASH_CACHE.clear(); java.nio.file.Files.walk(cacheDir.toPath()) .sorted(Comparator.reverseOrder()) .forEach(Errors.rethrow().wrap((Path path) -> java.nio.file.Files.delete(path))); @@ -191,6 +194,11 @@ private static void downloadRemote(String url, File dst) throws IOException { /** Returns either the filename safe URL, or (first40)--(Base64 filenamesafe)(last40). */ static String filenameSafe(String url) { + //preserve the filename and extension if query parameters are present in original url. + if (url.contains("?at")) { + String fileNameWithoutQuery = url.substring(0, url.indexOf("?at")); + url = String.format("%s-%s", url, fileNameWithoutQuery.substring(fileNameWithoutQuery.lastIndexOf("/") + 1)); + } String allSafeCharacters = url.replaceAll("[^a-zA-Z0-9-+_.]", "-"); String noDuplicateDash = allSafeCharacters.replaceAll("-+", "-"); if (noDuplicateDash.length() <= MAX_FILE_LENGTH) { @@ -253,7 +261,8 @@ private static void assertInitialized() { } } - static interface AuthPlugin { + @FunctionalInterface + interface AuthPlugin { void addAuthToken(String url, Request.Builder builder) throws MalformedURLException; } diff --git a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java index 373cb10..60a4128 100644 --- a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java +++ b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java @@ -17,13 +17,27 @@ import com.diffplug.common.base.Errors; +import com.google.gson.Gson; import groovy.lang.Closure; import java.io.File; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.Function; +import java.util.stream.Collectors; + import javax.annotation.Nullable; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Request.Builder; +import okhttp3.Response; +import okhttp3.ResponseBody; import org.jetbrains.annotations.NotNull; /** Configures where {@link Blowdryer#file(String)} downloads files from. */ @@ -32,6 +46,12 @@ public class BlowdryerSetup { private static final String GITHUB_HOST = "raw.githubusercontent.com"; private static final String GITLAB_HOST = "gitlab.com"; + private static final String BITBUCKET_HOST = "api.bitbucket.org/2.0/repositories"; + + static final Map BITBUCKET_COMMIT_HASH_CACHE = new HashMap<>(); + + private static final String HTTP_PROTOCOL = "http://"; + private static final String HTTPS_PROTOCOL = "https://"; private final File referenceDirectory; @@ -46,7 +66,7 @@ public BlowdryerSetup(File referenceDirectory) { /** * Default value is `src/main/resources`. If you change, you must change as the *first* call. - * + * * The nice thing about the default `src/main/resources` is that if you ever want to, you could * copy the blowdryer code into your blowdryer repo, and deploy your own plugin that pulls resources * from the local jar rather than from github. Keeping the default lets you switch to that approach @@ -58,7 +78,7 @@ public void repoSubfolder(String repoSubfolder) { } public enum GitAnchorType { - TAG, COMMIT, TREE + TAG, COMMIT, TREE, BRANCH } /** Sets the source where we will grab these scripts. */ @@ -68,8 +88,8 @@ public GitHub github(String repoOrg, GitAnchorType anchorType, String anchor) { } public class GitHub { - private String repoOrg; - private String anchor; + private final String repoOrg; + private final String anchor; private @Nullable String authToken; private GitHub(String repoOrg, String anchor) { @@ -86,7 +106,7 @@ public GitHub authToken(String authToken) { private GitHub setGlobals() { Blowdryer.setResourcePluginNull(); - String root = "https://" + GITHUB_HOST + "/" + repoOrg + "/" + anchor + "/"; + String root = HTTPS_PROTOCOL + GITHUB_HOST + "/" + repoOrg + "/" + anchor + "/"; Blowdryer.setResourcePlugin(resource -> root + getFullResourcePath(resource), authToken == null ? null : (url, builder) -> { if (url.startsWith(root)) { builder.addHeader("Authorization", "Bearer " + authToken); @@ -103,8 +123,8 @@ public GitLab gitlab(String repoOrg, GitAnchorType anchorType, String anchor) { } public class GitLab { - private String repoOrg; - private String anchor; + private final String repoOrg; + private final String anchor; private @Nullable String authToken; private String protocol, host; @@ -121,11 +141,11 @@ public GitLab authToken(String authToken) { } public GitLab customDomainHttp(String domain) { - return customProtocolAndDomain("http://", domain); + return customProtocolAndDomain(HTTP_PROTOCOL, domain); } public GitLab customDomainHttps(String domain) { - return customProtocolAndDomain("https://", domain); + return customProtocolAndDomain(HTTPS_PROTOCOL, domain); } private GitLab customProtocolAndDomain(String protocol, String domain) { @@ -147,6 +167,195 @@ private GitLab setGlobals() { } } + public enum BitbucketType { + CLOUD, SERVER + } + + /** Sets the source where we will grab these scripts. */ + public Bitbucket bitbucket(String repoOrg, GitAnchorType anchorType, String anchor) { + return new Bitbucket(repoOrg, anchorType, anchor, BitbucketType.CLOUD); + } + + public class Bitbucket { + + private final String repoOrg; + private final String repoName; + private final String anchor; + private final GitAnchorType anchorType; + private BitbucketType bitbucketType; + private @Nullable String authToken; + private String protocol, host; + + private Bitbucket(String repoOrg, GitAnchorType anchorType, String anchor, BitbucketType bitbucketType) { + Blowdryer.assertPluginNotSet(); + final String[] repoOrgAndName = assertNoLeadingOrTrailingSlash(repoOrg).split("/"); + if (repoOrgAndName.length != 2) { + throw new IllegalArgumentException("repoOrg must be in format 'repoOrg/repoName'"); + } + this.repoOrg = repoOrgAndName[0]; + this.repoName = repoOrgAndName[1]; + this.anchorType = anchorType; + this.bitbucketType = bitbucketType; + this.anchor = assertNoLeadingOrTrailingSlash(anchor); + customDomainHttps(BITBUCKET_HOST); + } + + /** + * Only supported for Bitbucket Server 5.5+ + * Format: "personalAccessToken" + */ + public Bitbucket server() { + this.bitbucketType = BitbucketType.SERVER; + return setGlobals(); + } + + public Bitbucket serverAuth(String personalAccessToken) { + this.authToken = String.format("Bearer %s", personalAccessToken); + this.bitbucketType = BitbucketType.SERVER; + return setGlobals(); + } + + /** + * Only available for Bitbucket Cloud. + * Format: "username:appPassword" + */ + public Bitbucket cloudAuth(String usernameAndAppPassword) { + final String encoding = Base64.getEncoder().encodeToString((usernameAndAppPassword) + .getBytes(StandardCharsets.UTF_8)); + this.authToken = String.format("Basic %s", encoding); + this.bitbucketType = BitbucketType.CLOUD; + return setGlobals(); + } + + public Bitbucket customDomainHttp(String domain) { + return customProtocolAndDomain(HTTP_PROTOCOL, domain); + } + + public Bitbucket customDomainHttps(String domain) { + return customProtocolAndDomain(HTTPS_PROTOCOL, domain); + } + + private Bitbucket customProtocolAndDomain(String protocol, String domain) { + this.protocol = protocol; + this.host = domain; + return setGlobals(); + } + + private Bitbucket setGlobals() { + Blowdryer.setResourcePluginNull(); + String urlStart = getUrlStart(); + + Blowdryer.setResourcePlugin(resource -> getFullUrl(urlStart, encodeUrlParts(getFullResourcePath(resource))), (url, builder) -> { + if (authToken != null) { + builder.addHeader("Authorization", authToken); + } + }); + return this; + } + + // Bitbucket Cloud and Bitbucket Server (premium, company hosted) has different url structures. + // Bitbucket Cloud uses "org/repo" in URLs, where org is your (or someone else's) account name. + // Bitbucket Server uses "projects/PROJECT_KEY/repos/REPO_NAME" in urls. + + private String getUrlStart() { + if (isServer()) { + return String.format("%s%s/projects/%s/repos/%s", protocol, host, repoOrg, repoName); + } else { + return String.format("%s%s/%s/%s", protocol, host, repoOrg, repoName); + } + } + + private String getFullUrl(String urlStart, String filePath) { + if (isServer()) { + return String.format("%s/raw/%s?at=%s", urlStart, filePath, encodeUrlPart(getAnchor())); + } else { + return String.format("%s/src/%s/%s", urlStart, encodeUrlParts(getAnchorForCloudAsHash()), filePath); + } + } + + private boolean isServer() { + return BitbucketType.SERVER.equals(this.bitbucketType); + } + + private String getAnchor() { + switch (anchorType) { + case TAG: + return String.format("refs/tags/%s", anchor); + case BRANCH: + return String.format("refs/heads/%s", anchor); + case TREE: + throw new UnsupportedOperationException("TREE hash resolution is not supported."); + case COMMIT: + default: + return anchor; + } + } + + private String getAnchorForCloudAsHash() { + switch (anchorType) { + case TAG: + return getCommitHash("refs/tags/"); + case BRANCH: + return getCommitHash("refs/branches/"); + case TREE: + throw new UnsupportedOperationException("TREE hash resolution is not supported."); + case COMMIT: + default: + return anchor; + } + } + + // Bitbucket API: https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Bworkspace%7D/%7Brepo_slug%7D/src/%7Bcommit%7D/%7Bpath%7D + private String getCommitHash(String baseRefs) { + String requestUrl = String.format("%s/%s%s", getUrlStart(), baseRefs, encodeUrlParts(anchor)); + + return BITBUCKET_COMMIT_HASH_CACHE.computeIfAbsent(requestUrl, url -> getCommitHashFromBitbucket(requestUrl)); + } + + private String getCommitHashFromBitbucket(String requestUrl) { + OkHttpClient client = new OkHttpClient.Builder().build(); + Builder requestBuilder = new Builder() + .url(requestUrl); + if (authToken != null) { + requestBuilder + .addHeader("Authorization", authToken); + } + Request request = requestBuilder.build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IllegalArgumentException(String.format("%s\nreceived http code %s \n %s", request.url(), response.code(), + Objects.requireNonNull(response.body()).string())); + } + try (ResponseBody body = response.body()) { + RefsTarget refsTarget = new Gson().fromJson(Objects.requireNonNull(body).string(), RefsTarget.class); + return refsTarget.target.hash; + } + } catch (Exception e) { + throw new IllegalArgumentException("Body was expected to be non-null"); + } + } + + private class RefsTarget { + + private final Target target; + + private RefsTarget(Target target) { + this.target = target; + } + + private class Target { + + private final String hash; + + private Target(String hash) { + this.hash = hash; + } + + } + } + } + /** * Uses the provided {@code jarFile} to extract a file resource. * @param jarFile Absolute path to JAR on the file system. @@ -214,4 +423,10 @@ private static String encodeUrlPart(String part) { throw new IllegalArgumentException("error encoding part", e); } } + + private static String encodeUrlParts(String part) { + return Arrays.stream(part.split("/")) + .map(BlowdryerSetup::encodeUrlPart) + .collect(Collectors.joining("/")); + } } diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java index 1829d8a..7893855 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java @@ -15,7 +15,6 @@ */ package com.diffplug.blowdryer; - import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -25,9 +24,17 @@ import org.junit.Ignore; import org.junit.Test; -@Ignore("has to be filled with prvate tokens and repos") +@Ignore("has to be filled with private tokens and repos") public class BlowdryerPluginAuthTest extends GradleHarness { + private static final String BITBUCKET_REPO_ORG = "bHack/blowdryer-private"; + private static final String BITBUCKET_REPO_USER = "bHack"; + private static final String BITBUCKET_REPO_APP_PW = "replace-with-app-pw"; + + private static final String BITBUCKET_REPO_PAT_REPO_ORG = "MNT/mnt-centralised-cfg"; + private static final String BITBUCKET_REPO_PAT = "replace-with-pat"; + private static final String BITBUCKET_PRIVATE_SERVER_HOST = "replace.with.private.host"; + private void settingsGitlabAuth(String tag, String... extra) throws IOException { write("settings.gradle", "plugins { id 'com.diffplug.blowdryerSetup' }", @@ -45,6 +52,24 @@ private void settingsGithubAuth(String tag, String... extra) throws IOException Arrays.stream(extra).collect(Collectors.joining("\n"))); } + private void settingsBitbucketBasicAuth(String tag, String... extra) throws IOException { + write("settings.gradle", + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s', 'tree', '%s').cloudAuth('%s:%s');", + BITBUCKET_REPO_ORG, tag, BITBUCKET_REPO_USER, BITBUCKET_REPO_APP_PW) + + " }", + Arrays.stream(extra).collect(Collectors.joining("\n"))); + } + + private void settingsBitbucketPersonalAccessTokenAuth(String tag, String... extra) throws IOException { + write("settings.gradle", + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s', 'tree', '%s').customDomainHttps('%s')" + + ".serverAuth('%s');", BITBUCKET_REPO_PAT_REPO_ORG, tag, BITBUCKET_PRIVATE_SERVER_HOST, BITBUCKET_REPO_PAT) + + " }", + Arrays.stream(extra).collect(Collectors.joining("\n"))); + } + @Test public void githubAuthTag() throws IOException { settingsGithubAuth("master"); @@ -63,6 +88,24 @@ public void gitlabAuthTag() throws IOException { gradleRunner().build(); } + @Test + public void bitbucketCloudAuth() throws IOException { + settingsBitbucketBasicAuth("master"); + write("build.gradle", + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'a'"); + gradleRunner().build(); + } + + @Test + public void bitbucketServerAuth() throws IOException { + settingsBitbucketPersonalAccessTokenAuth("master"); + write("build.gradle", + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('checkstyle/spotless.gradle').text == 'a'"); + gradleRunner().build(); + } + /** Writes the given content to the given path. */ protected File write(String path, String... lines) throws IOException { File file = file(path); diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java index 8e471c3..5699ec7 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java @@ -26,6 +26,8 @@ public class BlowdryerPluginTest extends GradleHarness { private static final String SETTINGS_GRADLE = "settings.gradle"; private static final String BUILD_GRADLE = "build.gradle"; + //todo: change to diffplug if created + private static final String BITBUCKET_REPO_ORG = "bHack"; private void settingsGithub(String tag, String... extra) throws IOException { write(SETTINGS_GRADLE, @@ -55,6 +57,28 @@ private void settingsGitlabRootFolder(String tag, String... extra) throws IOExce Arrays.stream(extra).collect(Collectors.joining("\n"))); } + private void settingsBitbucket(String tag, String... extra) throws IOException { + write(SETTINGS_GRADLE, + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s') }", BITBUCKET_REPO_ORG, tag), + Arrays.stream(extra).collect(Collectors.joining("\n"))); + } + + private void settingsCustomBitbucket(String tag, String... extra) throws IOException { + write(SETTINGS_GRADLE, + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s').customDomainHttps('api.bitbucket.org/2.0/repositories') }", + BITBUCKET_REPO_ORG, tag), + Arrays.stream(extra).collect(Collectors.joining("\n"))); + } + + private void settingsCustomBitbucket(String tag, int host, String... extra) throws IOException { + write(SETTINGS_GRADLE, + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s').customDomainHttps('%s') }", BITBUCKET_REPO_ORG, tag, host), + Arrays.stream(extra).collect(Collectors.joining("\n"))); + } + private void settingsLocalJar(String dependency) throws IOException { write(SETTINGS_GRADLE, "plugins { id 'com.diffplug.blowdryerSetup' }", @@ -113,6 +137,32 @@ public void gitlabTag() throws IOException { gradleRunner().buildAndFail(); } + @Test + public void bitbucketTag() throws IOException { + settingsBitbucket("test/2/a"); + write(BUILD_GRADLE, + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'a'", + "assert 干.prop('sample', 'name') == 'test'", + "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); + gradleRunner().build(); + + settingsBitbucket("test/2/b"); + write(BUILD_GRADLE, + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'b'", + "assert 干.prop('sample', 'name') == 'testB'", + "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); + gradleRunner().build(); + + // double-check that failures do fail + settingsBitbucket("test/2/b"); + write(BUILD_GRADLE, + "plugins { id 'com.diffplug.blowdryer' }", + "assert Blowdryer.file('sample').text == 'a'"); + gradleRunner().buildAndFail(); + } + @Test public void customGitlabTag() throws IOException { settingsCustomGitlab("test/2/a"); @@ -139,6 +189,39 @@ public void customGitlabTag() throws IOException { gradleRunner().buildAndFail(); } + @Test + public void customBitbucketTag() throws IOException { + settingsCustomBitbucket("test/2/a"); + write(BUILD_GRADLE, + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'a'", + "assert 干.prop('sample', 'name') == 'test'", + "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); + gradleRunner().build(); + + settingsCustomBitbucket("test/2/b"); + write(BUILD_GRADLE, + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'b'", + "assert 干.prop('sample', 'name') == 'testB'", + "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); + gradleRunner().build(); + + // double-check that failures do fail + settingsCustomBitbucket("test/2/b"); + write(BUILD_GRADLE, + "plugins { id 'com.diffplug.blowdryer' }", + "assert Blowdryer.file('sample').text == 'a'"); + gradleRunner().buildAndFail(); + + // unresolved host + settingsCustomBitbucket("test/2/b", 123); + write(BUILD_GRADLE, + "plugins { id 'com.diffplug.blowdryer' }", + "assert Blowdryer.file('sample').text == 'a'"); + gradleRunner().buildAndFail(); + } + @Test public void rootfolderGitlabTag() throws IOException { settingsGitlabRootFolder("test/2/a"); diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java index 3d126d6..16e53bf 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java @@ -15,10 +15,24 @@ */ package com.diffplug.blowdryer; - -import org.assertj.core.api.Assertions; +import com.diffplug.blowdryer.Blowdryer.AuthPlugin; +import com.diffplug.blowdryer.Blowdryer.ResourcePlugin; +import com.diffplug.blowdryer.BlowdryerSetup.Bitbucket; +import com.diffplug.blowdryer.BlowdryerSetup.GitAnchorType; +import okhttp3.Request; +import okhttp3.Request.Builder; import org.junit.Test; +import java.lang.reflect.Field; +import java.util.Base64; +import java.util.UUID; + +import static com.diffplug.blowdryer.BlowdryerSetup.BITBUCKET_COMMIT_HASH_CACHE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import static java.nio.charset.StandardCharsets.UTF_8; + public class BlowdryerTest { private static final String JAR_FILE_RESOURCE_SEPARATOR = "!/"; private static final String FILE_PROTOCOL = "file:///"; @@ -28,23 +42,166 @@ public void filenameSafe() { filenameSafe("http://shortName.com/a+b-0-9~Z", "http-shortName.com-a+b-0-9-Z"); filenameSafe("https://raw.githubusercontent.com/diffplug/durian-build/07f588e52eb0f31e596eab0228a5df7233a98a14/gradle/spotless/spotless.license.java", "https-raw.githubusercontent.com-diffplug--3vpUTw--14-gradle-spotless-spotless.license.java"); + filenameSafe("https://raw.githubusercontent.com/diffplug/durian-build/07f588e52eb0f31e596eab0228a5df7233a98a14/gradle/spotless/spotless.license.java?at=refs/heads/master", + "https-raw.githubusercontent.com-diffplug--Sg5TNw---refs-heads-master-spotless.license.java"); + filenameSafe("https://raw.githubusercontent.com/diffplug/durian-build/07f588e52eb0f31e596eab0228a5df7233a98a14/gradle/spotless/spotless.license.java?at=refs%2Fheads%2Fmaster", + "https-raw.githubusercontent.com-diffplug--s+tWqw--s-2Fheads-2Fmaster-spotless.license.java"); } private void filenameSafe(String url, String safe) { - Assertions.assertThat(Blowdryer.filenameSafe(url)).isEqualTo(safe); + assertThat(Blowdryer.filenameSafe(url)).isEqualTo(safe); } @Test public void cachedFileDeleted_issue_11() { String test = "https://raw.githubusercontent.com/diffplug/blowdryer/test/2/b/src/main/resources/sample"; - Assertions.assertThat(Blowdryer.immutableUrl(test)).hasContent("b"); + assertThat(Blowdryer.immutableUrl(test)).hasContent("b"); Blowdryer.immutableUrl(test).delete(); - Assertions.assertThat(Blowdryer.immutableUrl(test)).hasContent("b"); + assertThat(Blowdryer.immutableUrl(test)).hasContent("b"); } @Test public void immutableUrlOfLocalJar() { String jarFile = BlowdryerPluginTest.class.getResource("test.jar").getFile(); - Assertions.assertThat(Blowdryer.immutableUrl(FILE_PROTOCOL + jarFile + JAR_FILE_RESOURCE_SEPARATOR + "sample")).exists(); + assertThat(Blowdryer.immutableUrl(FILE_PROTOCOL + jarFile + JAR_FILE_RESOURCE_SEPARATOR + "sample")).exists(); } + + @Test + public void bitbucketCloud_tagAnchorType() throws Exception { + final String hashRequestUrl = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/refs/tags/testAnchor"; + final String hash = UUID.randomUUID().toString(); + final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/" + hash + "/src/main/resources/test.properties"; + BITBUCKET_COMMIT_HASH_CACHE.put(hashRequestUrl, hash); + + setupBitbucketTestTarget(GitAnchorType.TAG).cloudAuth("un:pw"); + final ResourcePlugin target = getResourcePlugin(); + + assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); + } + + @Test + public void bitbucketCloud_commitAnchorType() throws Exception { + final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/testAnchor/src/main/resources/test.properties"; + setupBitbucketTestTarget(GitAnchorType.COMMIT); + final ResourcePlugin target = getResourcePlugin(); + + assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); + } + + @Test + public void bitbucketCloud_branchAnchorType() throws Exception { + final String hashRequestUrl = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/refs/branches/testAnchor"; + final String hash = UUID.randomUUID().toString(); + final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/" + hash + "/src/main/resources/test.properties"; + BITBUCKET_COMMIT_HASH_CACHE.put(hashRequestUrl, hash); + + setupBitbucketTestTarget(GitAnchorType.BRANCH).cloudAuth("un:pw"); + final ResourcePlugin target = getResourcePlugin(); + + assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); + } + + @Test + public void bitbucketCloud_treeAnchorType() throws Exception { + setupBitbucketTestTarget(GitAnchorType.TREE); + final ResourcePlugin target = getResourcePlugin(); + + assertThatThrownBy(() -> target.toImmutableUrl("test.properties")) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage("TREE hash resolution is not supported."); + } + + @Test + public void bitbucketServer_tagAnchorType() throws Exception { + final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=refs%2Ftags%2FtestAnchor"; + setupBitbucketTestTarget(GitAnchorType.TAG).server().customDomainHttps("my.bitbucket.com"); + final ResourcePlugin target = getResourcePlugin(); + + assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); + } + + @Test + public void bitbucketServer_commitAnchorType() throws Exception { + final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=testAnchor"; + setupBitbucketTestTarget(GitAnchorType.COMMIT).server().customDomainHttps("my.bitbucket.com"); + final ResourcePlugin target = getResourcePlugin(); + + assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); + } + + @Test + public void bitbucketServer_branchAnchorType() throws Exception { + final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=refs%2Fheads%2FtestAnchor"; + setupBitbucketTestTarget(GitAnchorType.BRANCH).server().customDomainHttps("my.bitbucket.com"); + final ResourcePlugin target = getResourcePlugin(); + + assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); + } + + @Test + public void bitbucketServer_treeAnchorType() throws Exception { + setupBitbucketTestTarget(GitAnchorType.TREE).server().customDomainHttps("my.bitbucket.com"); + final ResourcePlugin target = getResourcePlugin(); + + assertThatThrownBy(() -> target.toImmutableUrl("test.properties")) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage("TREE hash resolution is not supported."); + } + + @Test + public void bitbucketServerAuth() throws Exception { + final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=refs%2Fheads%2FtestAnchor"; + final String personalAccessToken = randomUUID(); + setupBitbucketTestTarget(GitAnchorType.BRANCH).serverAuth(personalAccessToken).customDomainHttps("my.bitbucket.com"); + final ResourcePlugin target = getResourcePlugin(); + + final AuthPlugin otherTarget = getAuthPlugin(); + final Builder requestBuilder = new Builder().url(expected); + otherTarget.addAuthToken(expected, requestBuilder); + final Request request = requestBuilder.build(); + + assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); + assertThat(request.header("Authorization")).isEqualTo(String.format("Bearer %s", personalAccessToken)); + } + + @Test + public void bitbucketCloudAuth() throws Exception { + final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/testAnchor/src/main/resources/test.properties"; + final String usernameAndAppPassword = String.format("%s:%s", randomUUID(), randomUUID()); + setupBitbucketTestTarget(GitAnchorType.COMMIT).cloudAuth(usernameAndAppPassword); + + final ResourcePlugin target = getResourcePlugin(); + final AuthPlugin otherTarget = getAuthPlugin(); + final Builder requestBuilder = new Builder().url(expected); + otherTarget.addAuthToken(expected, requestBuilder); + final Request request = requestBuilder.build(); + + assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); + final String encoded = Base64.getEncoder().encodeToString((usernameAndAppPassword) + .getBytes(UTF_8)); + assertThat(request.header("Authorization")).isEqualTo(String.format("Basic %s", encoded)); + } + + private Bitbucket setupBitbucketTestTarget(final GitAnchorType anchorType) { + final String repoOrg = "testOrg/testRepo"; + final String anchor = "testAnchor"; + return new BlowdryerSetup(null).bitbucket(repoOrg, anchorType, anchor); + } + + private ResourcePlugin getResourcePlugin() throws Exception { + final Field field = Blowdryer.class.getDeclaredField("plugin"); + field.setAccessible(true); + return (ResourcePlugin) field.get(null); + } + + private AuthPlugin getAuthPlugin() throws Exception { + final Field field = Blowdryer.class.getDeclaredField("authPlugin"); + field.setAccessible(true); + return (AuthPlugin) field.get(null); + } + + private String randomUUID() { + return UUID.randomUUID().toString(); + } + } diff --git a/src/test/java/com/diffplug/blowdryer/GradleHarness.java b/src/test/java/com/diffplug/blowdryer/GradleHarness.java index b37600c..374c79a 100644 --- a/src/test/java/com/diffplug/blowdryer/GradleHarness.java +++ b/src/test/java/com/diffplug/blowdryer/GradleHarness.java @@ -24,4 +24,9 @@ public class GradleHarness extends ResourceHarness { protected GradleRunner gradleRunner() throws IOException { return GradleRunner.create().withProjectDir(rootFolder()).withPluginClasspath(); } + + /** A GradleRunner with debugger attached. */ + protected GradleRunner gradleRunnerDebug() throws IOException { + return GradleRunner.create().withProjectDir(rootFolder()).withDebug(true).withPluginClasspath(); + } } From c673440b4f1b7e2cd750e81ef5b7238aa89bfa6c Mon Sep 17 00:00:00 2001 From: Sergey Sklarovs Date: Mon, 21 Jun 2021 22:41:51 +0200 Subject: [PATCH 02/14] Add link in CHANGELOG.md to the PR. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a91519..cc43dde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `filenameSafe` now removes query parameters and appends file name to the end of the link to preserve its original file extension. -- Support for Bitbucket Cloud and Server ([#20](add-link-to-PR)). +- Support for Bitbucket Cloud and Server ([#23](https://github.com/diffplug/blowdryer/pull/23)). ## [1.2.1] - 2021-06-01 ### Fixed From d10dd7da1bdd6b7d1a192a14df3fd894682e4a5e Mon Sep 17 00:00:00 2001 From: Sergey Sklarovs Date: Tue, 22 Jun 2021 11:18:52 +0200 Subject: [PATCH 03/14] Address PR comments. --- CHANGELOG.md | 2 +- README.md | 73 +++---------------- build.gradle | 1 + .../com/diffplug/blowdryer/Blowdryer.java | 21 ++++-- .../diffplug/blowdryer/BlowdryerSetup.java | 64 ++++++++-------- .../blowdryer/BlowdryerPluginAuthTest.java | 4 +- .../com/diffplug/blowdryer/BlowdryerTest.java | 53 ++------------ .../com/diffplug/blowdryer/GradleHarness.java | 4 - 8 files changed, 67 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc43dde..136e69d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- `filenameSafe` now removes query parameters and appends file name to the end of the link to preserve its original file extension. +- `filenameSafe` now, if required, removes query parameters (for Bitbucket) and appends file name to the end of the link to preserve its original file extension. - Support for Bitbucket Cloud and Server ([#23](https://github.com/diffplug/blowdryer/pull/23)). ## [1.2.1] - 2021-06-01 diff --git a/README.md b/README.md index 80b5df1..2d6960e 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,21 @@ blowdryerSetup { github('acme/blowdryer-acme', 'tag', 'v1.4.5') // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' // or 'tree', 'a5df7233a98a1407f588e52eb0f31e596eab0228' + // or gitlab('acme/blowdryer-acme', 'tag', 'v1.4.5').customDomainHttp('acme.org').authToken('abc123') + + // Public/Private Bitbucket Cloud repo configuration. Add .cloudAuth only for private repos. + // bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').cloudAuth("username:appPassword") + + // Public/Private Bitbucket Server repo configurations. User .server for public repos. Use .serverAuth for private repos. + // bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').server().customDomainHttps('my.bitbucket.company.domain.com') + // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').serverAuth('personalAccessToken').customDomainHttps('my.bitbucket.company.domain.com') } ``` +* Reference on how to create [application password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) +for Bitbucket Cloud private repo access.
+* Reference on how to create [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html) +for Bitbucket Server private repo access. Now, in *only* your root `build.gradle`, do this: `apply plugin: 'com.diffplug.blowdryer'`. Now, in any project throughout your gradle build (including subprojects), you can do this: @@ -72,67 +84,6 @@ somePlugin { `Blowdryer.prop()` parses a java `.properties` file which was downloaded using `Blowdryer.file()`, and then returns the value associated with the given key. -### Bitbucket support -#### Bitbucket Cloud - -For public Bitbucket Cloud repo, use such configuration: -```gradle -plugins { - id 'com.diffplug.blowdryerSetup' version '1.2.2' -} - -blowdryerSetup { - bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5') - // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' - // or 'branch', 'feature/branch' -} -``` - -For private Bitbucket Cloud repo, use such configuration: -```gradle -plugins { - id 'com.diffplug.blowdryerSetup' version '1.2.2' -} - -blowdryerSetup { - bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').cloudAuth("username:appPassword") - // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' - // or 'branch', 'feature/branch' -} -``` -Reference on how to create [application password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/). - -#### Bitbucket Server - -For public Bitbucket Server repo, use such configuration: -```gradle -plugins { - id 'com.diffplug.blowdryerSetup' version '1.2.2' -} - -blowdryerSetup { - bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').server() - // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').server().customDomainHttps('my.bitbucket.company.domain.com') - // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' - // or 'branch', 'feature/branch' -} -``` - -For private Bitbucket Server repo, use such configuration: -```gradle -plugins { - id 'com.diffplug.blowdryerSetup' version '1.2.2' -} - -blowdryerSetup { - bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').serverAuth('personalAccessToken') - // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').serverAuth('personalAccessToken').customDomainHttps('my.bitbucket.company.domain.com') - // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' - // or 'branch', 'feature/branch' -} -``` -Reference on how to create [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). - ### Chinese for "dry" (干) If you like brevity and unicode, you can replace `Blowdryer` with `干`. We'll use `干` throughout the rest of the readme, but you can find-replace `干` with `Blowdryer` and get the same results. diff --git a/build.gradle b/build.gradle index 67234ee..43bfabc 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.2.2' implementation 'com.squareup.okio:okio:2.4.1' implementation 'com.google.code.gson:gson:2.8.7' + implementation 'org.mockito:mockito-core:3.11.2' implementation 'com.diffplug.durian:durian-core:1.2.0' implementation 'com.diffplug.durian:durian-io:1.2.0' testImplementation 'junit:junit:4.12' diff --git a/src/main/java/com/diffplug/blowdryer/Blowdryer.java b/src/main/java/com/diffplug/blowdryer/Blowdryer.java index 5f08398..baf0067 100644 --- a/src/main/java/com/diffplug/blowdryer/Blowdryer.java +++ b/src/main/java/com/diffplug/blowdryer/Blowdryer.java @@ -47,8 +47,6 @@ import okio.Okio; import org.gradle.api.Project; -import static com.diffplug.blowdryer.BlowdryerSetup.BITBUCKET_COMMIT_HASH_CACHE; - /** * Public static methods which retrieve resources as * determined by {@link BlowdryerSetup}. @@ -74,7 +72,6 @@ static void wipeEntireCache() { try { urlToContent.clear(); fileToProps.clear(); - BITBUCKET_COMMIT_HASH_CACHE.clear(); java.nio.file.Files.walk(cacheDir.toPath()) .sorted(Comparator.reverseOrder()) .forEach(Errors.rethrow().wrap((Path path) -> java.nio.file.Files.delete(path))); @@ -194,11 +191,7 @@ private static void downloadRemote(String url, File dst) throws IOException { /** Returns either the filename safe URL, or (first40)--(Base64 filenamesafe)(last40). */ static String filenameSafe(String url) { - //preserve the filename and extension if query parameters are present in original url. - if (url.contains("?at")) { - String fileNameWithoutQuery = url.substring(0, url.indexOf("?at")); - url = String.format("%s-%s", url, fileNameWithoutQuery.substring(fileNameWithoutQuery.lastIndexOf("/") + 1)); - } + url = detectAndRewriteBitbucketUrlIfRequired(url); String allSafeCharacters = url.replaceAll("[^a-zA-Z0-9-+_.]", "-"); String noDuplicateDash = allSafeCharacters.replaceAll("-+", "-"); if (noDuplicateDash.length() <= MAX_FILE_LENGTH) { @@ -217,6 +210,18 @@ static String filenameSafe(String url) { } } + // preserve the filename and extension if query parameters are present in original url. + // required to retrieve XML files. + // From: https://mycompany.bitbucket.com/projects/PRJ/repos/my-repo/raw/src/main/resources/checkstyle/spotless.gradle?at=07f588e52eb0f31e596eab0228a5df7233a98a14 + // To: https://mycompany.bitbucket.com/projects/PRJ/repos/my-repo/raw/src/main/resources/checkstyle/spotless.gradle?at=07f588e52eb0f31e596eab0228a5df7233a98a14-spotless.gradle + private static String detectAndRewriteBitbucketUrlIfRequired(String url) { + if (url.contains("?at")) { + String fileNameWithoutQuery = url.substring(0, url.indexOf("?at")); + url = String.format("%s-%s", url, fileNameWithoutQuery.substring(fileNameWithoutQuery.lastIndexOf("/") + 1)); + } + return url; + } + ////////////////////// // plugin interface // ////////////////////// diff --git a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java index 60a4128..fd7e38a 100644 --- a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java +++ b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java @@ -16,6 +16,7 @@ package com.diffplug.blowdryer; +import com.diffplug.common.annotations.VisibleForTesting; import com.diffplug.common.base.Errors; import com.google.gson.Gson; import groovy.lang.Closure; @@ -25,8 +26,6 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; @@ -48,8 +47,6 @@ public class BlowdryerSetup { private static final String GITLAB_HOST = "gitlab.com"; private static final String BITBUCKET_HOST = "api.bitbucket.org/2.0/repositories"; - static final Map BITBUCKET_COMMIT_HASH_CACHE = new HashMap<>(); - private static final String HTTP_PROTOCOL = "http://"; private static final String HTTPS_PROTOCOL = "https://"; @@ -78,7 +75,7 @@ public void repoSubfolder(String repoSubfolder) { } public enum GitAnchorType { - TAG, COMMIT, TREE, BRANCH + TAG, COMMIT, TREE } /** Sets the source where we will grab these scripts. */ @@ -88,8 +85,8 @@ public GitHub github(String repoOrg, GitAnchorType anchorType, String anchor) { } public class GitHub { - private final String repoOrg; - private final String anchor; + private String repoOrg; + private String anchor; private @Nullable String authToken; private GitHub(String repoOrg, String anchor) { @@ -123,8 +120,8 @@ public GitLab gitlab(String repoOrg, GitAnchorType anchorType, String anchor) { } public class GitLab { - private final String repoOrg; - private final String anchor; + private String repoOrg; + private String anchor; private @Nullable String authToken; private String protocol, host; @@ -178,10 +175,10 @@ public Bitbucket bitbucket(String repoOrg, GitAnchorType anchorType, String anch public class Bitbucket { - private final String repoOrg; - private final String repoName; - private final String anchor; - private final GitAnchorType anchorType; + private String repoOrg; + private String repoName; + private String anchor; + private GitAnchorType anchorType; private BitbucketType bitbucketType; private @Nullable String authToken; private String protocol, host; @@ -279,29 +276,25 @@ private boolean isServer() { private String getAnchor() { switch (anchorType) { + case COMMIT: + return anchor; case TAG: return String.format("refs/tags/%s", anchor); - case BRANCH: - return String.format("refs/heads/%s", anchor); - case TREE: - throw new UnsupportedOperationException("TREE hash resolution is not supported."); - case COMMIT: default: - return anchor; + throw new UnsupportedOperationException(String.format("%s hash resolution is not supported.", anchorType)); } } private String getAnchorForCloudAsHash() { switch (anchorType) { + case COMMIT: + return anchor; case TAG: - return getCommitHash("refs/tags/"); - case BRANCH: - return getCommitHash("refs/branches/"); - case TREE: - throw new UnsupportedOperationException("TREE hash resolution is not supported."); - case COMMIT: + anchor = getCommitHash("refs/tags/"); + anchorType = GitAnchorType.COMMIT; + return anchor; default: - return anchor; + throw new UnsupportedOperationException("TREE hash resolution is not supported."); } } @@ -309,10 +302,11 @@ private String getAnchorForCloudAsHash() { private String getCommitHash(String baseRefs) { String requestUrl = String.format("%s/%s%s", getUrlStart(), baseRefs, encodeUrlParts(anchor)); - return BITBUCKET_COMMIT_HASH_CACHE.computeIfAbsent(requestUrl, url -> getCommitHashFromBitbucket(requestUrl)); + return getCommitHashFromBitbucket(requestUrl); } - private String getCommitHashFromBitbucket(String requestUrl) { + @VisibleForTesting + String getCommitHashFromBitbucket(String requestUrl) { OkHttpClient client = new OkHttpClient.Builder().build(); Builder requestBuilder = new Builder() .url(requestUrl); @@ -332,10 +326,17 @@ private String getCommitHashFromBitbucket(String requestUrl) { return refsTarget.target.hash; } } catch (Exception e) { - throw new IllegalArgumentException("Body was expected to be non-null"); + throw new IllegalArgumentException("Body was expected to be non-null", e); } } + // Do not encode '/'. + private String encodeUrlParts(String part) { + return Arrays.stream(part.split("/")) + .map(BlowdryerSetup::encodeUrlPart) + .collect(Collectors.joining("/")); + } + private class RefsTarget { private final Target target; @@ -424,9 +425,4 @@ private static String encodeUrlPart(String part) { } } - private static String encodeUrlParts(String part) { - return Arrays.stream(part.split("/")) - .map(BlowdryerSetup::encodeUrlPart) - .collect(Collectors.joining("/")); - } } diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java index 7893855..c224cd8 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java @@ -55,7 +55,7 @@ private void settingsGithubAuth(String tag, String... extra) throws IOException private void settingsBitbucketBasicAuth(String tag, String... extra) throws IOException { write("settings.gradle", "plugins { id 'com.diffplug.blowdryerSetup' }", - String.format("blowdryerSetup { bitbucket('%s', 'tree', '%s').cloudAuth('%s:%s');", + String.format("blowdryerSetup { bitbucket('%s', 'commit', '%s').cloudAuth('%s:%s');", BITBUCKET_REPO_ORG, tag, BITBUCKET_REPO_USER, BITBUCKET_REPO_APP_PW) + " }", Arrays.stream(extra).collect(Collectors.joining("\n"))); @@ -64,7 +64,7 @@ private void settingsBitbucketBasicAuth(String tag, String... extra) throws IOEx private void settingsBitbucketPersonalAccessTokenAuth(String tag, String... extra) throws IOException { write("settings.gradle", "plugins { id 'com.diffplug.blowdryerSetup' }", - String.format("blowdryerSetup { bitbucket('%s', 'tree', '%s').customDomainHttps('%s')" + String.format("blowdryerSetup { bitbucket('%s', 'commit', '%s').customDomainHttps('%s')" + ".serverAuth('%s');", BITBUCKET_REPO_PAT_REPO_ORG, tag, BITBUCKET_PRIVATE_SERVER_HOST, BITBUCKET_REPO_PAT) + " }", Arrays.stream(extra).collect(Collectors.joining("\n"))); diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java index 16e53bf..16d2fe2 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java @@ -27,9 +27,10 @@ import java.util.Base64; import java.util.UUID; -import static com.diffplug.blowdryer.BlowdryerSetup.BITBUCKET_COMMIT_HASH_CACHE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; import static java.nio.charset.StandardCharsets.UTF_8; @@ -42,10 +43,10 @@ public void filenameSafe() { filenameSafe("http://shortName.com/a+b-0-9~Z", "http-shortName.com-a+b-0-9-Z"); filenameSafe("https://raw.githubusercontent.com/diffplug/durian-build/07f588e52eb0f31e596eab0228a5df7233a98a14/gradle/spotless/spotless.license.java", "https-raw.githubusercontent.com-diffplug--3vpUTw--14-gradle-spotless-spotless.license.java"); - filenameSafe("https://raw.githubusercontent.com/diffplug/durian-build/07f588e52eb0f31e596eab0228a5df7233a98a14/gradle/spotless/spotless.license.java?at=refs/heads/master", - "https-raw.githubusercontent.com-diffplug--Sg5TNw---refs-heads-master-spotless.license.java"); - filenameSafe("https://raw.githubusercontent.com/diffplug/durian-build/07f588e52eb0f31e596eab0228a5df7233a98a14/gradle/spotless/spotless.license.java?at=refs%2Fheads%2Fmaster", - "https-raw.githubusercontent.com-diffplug--s+tWqw--s-2Fheads-2Fmaster-spotless.license.java"); + filenameSafe("https://mycompany.bitbucket.com/projects/PRJ/repos/my-repo/raw/src/main/resources/checkstyle/spotless.gradle?at=refs%2Fheads%2Fmaster", + "https-mycompany.bitbucket.com-projects-P--7T3UGg--at-refs-2Fheads-2Fmaster-spotless.gradle"); + filenameSafe("https://mycompany.bitbucket.com/projects/PRJ/repos/my-repo/raw/src/main/resources/checkstyle/spotless.gradle?at=07f588e52eb0f31e596eab0228a5df7233a98a14", + "https-mycompany.bitbucket.com-projects-P--K+HRow--596eab0228a5df7233a98a14-spotless.gradle"); } private void filenameSafe(String url, String safe) { @@ -71,9 +72,9 @@ public void bitbucketCloud_tagAnchorType() throws Exception { final String hashRequestUrl = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/refs/tags/testAnchor"; final String hash = UUID.randomUUID().toString(); final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/" + hash + "/src/main/resources/test.properties"; - BITBUCKET_COMMIT_HASH_CACHE.put(hashRequestUrl, hash); - setupBitbucketTestTarget(GitAnchorType.TAG).cloudAuth("un:pw"); + Bitbucket spy = spy(setupBitbucketTestTarget(GitAnchorType.TAG)).cloudAuth("un:pw"); + doReturn(hash).when(spy).getCommitHashFromBitbucket(hashRequestUrl); final ResourcePlugin target = getResourcePlugin(); assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); @@ -88,19 +89,6 @@ public void bitbucketCloud_commitAnchorType() throws Exception { assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); } - @Test - public void bitbucketCloud_branchAnchorType() throws Exception { - final String hashRequestUrl = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/refs/branches/testAnchor"; - final String hash = UUID.randomUUID().toString(); - final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/" + hash + "/src/main/resources/test.properties"; - BITBUCKET_COMMIT_HASH_CACHE.put(hashRequestUrl, hash); - - setupBitbucketTestTarget(GitAnchorType.BRANCH).cloudAuth("un:pw"); - final ResourcePlugin target = getResourcePlugin(); - - assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); - } - @Test public void bitbucketCloud_treeAnchorType() throws Exception { setupBitbucketTestTarget(GitAnchorType.TREE); @@ -129,15 +117,6 @@ public void bitbucketServer_commitAnchorType() throws Exception { assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); } - @Test - public void bitbucketServer_branchAnchorType() throws Exception { - final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=refs%2Fheads%2FtestAnchor"; - setupBitbucketTestTarget(GitAnchorType.BRANCH).server().customDomainHttps("my.bitbucket.com"); - final ResourcePlugin target = getResourcePlugin(); - - assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); - } - @Test public void bitbucketServer_treeAnchorType() throws Exception { setupBitbucketTestTarget(GitAnchorType.TREE).server().customDomainHttps("my.bitbucket.com"); @@ -148,22 +127,6 @@ public void bitbucketServer_treeAnchorType() throws Exception { .hasMessage("TREE hash resolution is not supported."); } - @Test - public void bitbucketServerAuth() throws Exception { - final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=refs%2Fheads%2FtestAnchor"; - final String personalAccessToken = randomUUID(); - setupBitbucketTestTarget(GitAnchorType.BRANCH).serverAuth(personalAccessToken).customDomainHttps("my.bitbucket.com"); - final ResourcePlugin target = getResourcePlugin(); - - final AuthPlugin otherTarget = getAuthPlugin(); - final Builder requestBuilder = new Builder().url(expected); - otherTarget.addAuthToken(expected, requestBuilder); - final Request request = requestBuilder.build(); - - assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); - assertThat(request.header("Authorization")).isEqualTo(String.format("Bearer %s", personalAccessToken)); - } - @Test public void bitbucketCloudAuth() throws Exception { final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/testAnchor/src/main/resources/test.properties"; diff --git a/src/test/java/com/diffplug/blowdryer/GradleHarness.java b/src/test/java/com/diffplug/blowdryer/GradleHarness.java index 374c79a..7401a8c 100644 --- a/src/test/java/com/diffplug/blowdryer/GradleHarness.java +++ b/src/test/java/com/diffplug/blowdryer/GradleHarness.java @@ -25,8 +25,4 @@ protected GradleRunner gradleRunner() throws IOException { return GradleRunner.create().withProjectDir(rootFolder()).withPluginClasspath(); } - /** A GradleRunner with debugger attached. */ - protected GradleRunner gradleRunnerDebug() throws IOException { - return GradleRunner.create().withProjectDir(rootFolder()).withDebug(true).withPluginClasspath(); - } } From 8bd183b80d40a3d2265efdd8b00763e258900283 Mon Sep 17 00:00:00 2001 From: Sergey Sklarovs Date: Tue, 22 Jun 2021 11:23:58 +0200 Subject: [PATCH 04/14] Spotless apply style changes --- README.md | 12 +-- .../diffplug/blowdryer/BlowdryerSetup.java | 80 +++++++++---------- .../blowdryer/BlowdryerPluginAuthTest.java | 29 +++---- .../blowdryer/BlowdryerPluginTest.java | 64 +++++++-------- .../com/diffplug/blowdryer/BlowdryerTest.java | 36 ++++----- .../com/diffplug/blowdryer/GradleHarness.java | 2 +- 6 files changed, 110 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 2d6960e..f481788 100644 --- a/README.md +++ b/README.md @@ -54,20 +54,20 @@ blowdryerSetup { github('acme/blowdryer-acme', 'tag', 'v1.4.5') // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' // or 'tree', 'a5df7233a98a1407f588e52eb0f31e596eab0228' - + // or gitlab('acme/blowdryer-acme', 'tag', 'v1.4.5').customDomainHttp('acme.org').authToken('abc123') - - // Public/Private Bitbucket Cloud repo configuration. Add .cloudAuth only for private repos. + + // Public/Private Bitbucket Cloud repo configuration. Add .cloudAuth only for private repos. // bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').cloudAuth("username:appPassword") - + // Public/Private Bitbucket Server repo configurations. User .server for public repos. Use .serverAuth for private repos. // bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').server().customDomainHttps('my.bitbucket.company.domain.com') // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').serverAuth('personalAccessToken').customDomainHttps('my.bitbucket.company.domain.com') } ``` -* Reference on how to create [application password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) +* Reference on how to create [application password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) for Bitbucket Cloud private repo access.
-* Reference on how to create [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html) +* Reference on how to create [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html) for Bitbucket Server private repo access. Now, in *only* your root `build.gradle`, do this: `apply plugin: 'com.diffplug.blowdryer'`. Now, in any project throughout your gradle build (including subprojects), you can do this: diff --git a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java index fd7e38a..192557b 100644 --- a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java +++ b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java @@ -29,9 +29,7 @@ import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; - import javax.annotation.Nullable; - import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Request.Builder; @@ -218,7 +216,7 @@ public Bitbucket serverAuth(String personalAccessToken) { */ public Bitbucket cloudAuth(String usernameAndAppPassword) { final String encoding = Base64.getEncoder().encodeToString((usernameAndAppPassword) - .getBytes(StandardCharsets.UTF_8)); + .getBytes(StandardCharsets.UTF_8)); this.authToken = String.format("Basic %s", encoding); this.bitbucketType = BitbucketType.CLOUD; return setGlobals(); @@ -276,50 +274,50 @@ private boolean isServer() { private String getAnchor() { switch (anchorType) { - case COMMIT: - return anchor; - case TAG: - return String.format("refs/tags/%s", anchor); - default: - throw new UnsupportedOperationException(String.format("%s hash resolution is not supported.", anchorType)); + case COMMIT: + return anchor; + case TAG: + return String.format("refs/tags/%s", anchor); + default: + throw new UnsupportedOperationException(String.format("%s hash resolution is not supported.", anchorType)); } } - private String getAnchorForCloudAsHash() { - switch (anchorType) { - case COMMIT: - return anchor; - case TAG: - anchor = getCommitHash("refs/tags/"); - anchorType = GitAnchorType.COMMIT; - return anchor; - default: - throw new UnsupportedOperationException("TREE hash resolution is not supported."); - } - } + private String getAnchorForCloudAsHash() { + switch (anchorType) { + case COMMIT: + return anchor; + case TAG: + anchor = getCommitHash("refs/tags/"); + anchorType = GitAnchorType.COMMIT; + return anchor; + default: + throw new UnsupportedOperationException("TREE hash resolution is not supported."); + } + } // Bitbucket API: https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Bworkspace%7D/%7Brepo_slug%7D/src/%7Bcommit%7D/%7Bpath%7D private String getCommitHash(String baseRefs) { String requestUrl = String.format("%s/%s%s", getUrlStart(), baseRefs, encodeUrlParts(anchor)); - return getCommitHashFromBitbucket(requestUrl); - } + return getCommitHashFromBitbucket(requestUrl); + } - @VisibleForTesting + @VisibleForTesting String getCommitHashFromBitbucket(String requestUrl) { OkHttpClient client = new OkHttpClient.Builder().build(); Builder requestBuilder = new Builder() - .url(requestUrl); + .url(requestUrl); if (authToken != null) { requestBuilder - .addHeader("Authorization", authToken); + .addHeader("Authorization", authToken); } Request request = requestBuilder.build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IllegalArgumentException(String.format("%s\nreceived http code %s \n %s", request.url(), response.code(), - Objects.requireNonNull(response.body()).string())); + Objects.requireNonNull(response.body()).string())); } try (ResponseBody body = response.body()) { RefsTarget refsTarget = new Gson().fromJson(Objects.requireNonNull(body).string(), RefsTarget.class); @@ -333,29 +331,29 @@ String getCommitHashFromBitbucket(String requestUrl) { // Do not encode '/'. private String encodeUrlParts(String part) { return Arrays.stream(part.split("/")) - .map(BlowdryerSetup::encodeUrlPart) - .collect(Collectors.joining("/")); + .map(BlowdryerSetup::encodeUrlPart) + .collect(Collectors.joining("/")); } private class RefsTarget { - private final Target target; + private final Target target; - private RefsTarget(Target target) { - this.target = target; - } + private RefsTarget(Target target) { + this.target = target; + } - private class Target { + private class Target { - private final String hash; + private final String hash; - private Target(String hash) { - this.hash = hash; - } + private Target(String hash) { + this.hash = hash; + } - } - } - } + } + } + } /** * Uses the provided {@code jarFile} to extract a file resource. diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java index c224cd8..4995356 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java @@ -15,6 +15,7 @@ */ package com.diffplug.blowdryer; + import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -54,20 +55,20 @@ private void settingsGithubAuth(String tag, String... extra) throws IOException private void settingsBitbucketBasicAuth(String tag, String... extra) throws IOException { write("settings.gradle", - "plugins { id 'com.diffplug.blowdryerSetup' }", - String.format("blowdryerSetup { bitbucket('%s', 'commit', '%s').cloudAuth('%s:%s');", - BITBUCKET_REPO_ORG, tag, BITBUCKET_REPO_USER, BITBUCKET_REPO_APP_PW) - + " }", - Arrays.stream(extra).collect(Collectors.joining("\n"))); + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s', 'commit', '%s').cloudAuth('%s:%s');", + BITBUCKET_REPO_ORG, tag, BITBUCKET_REPO_USER, BITBUCKET_REPO_APP_PW) + + " }", + Arrays.stream(extra).collect(Collectors.joining("\n"))); } private void settingsBitbucketPersonalAccessTokenAuth(String tag, String... extra) throws IOException { write("settings.gradle", - "plugins { id 'com.diffplug.blowdryerSetup' }", - String.format("blowdryerSetup { bitbucket('%s', 'commit', '%s').customDomainHttps('%s')" - + ".serverAuth('%s');", BITBUCKET_REPO_PAT_REPO_ORG, tag, BITBUCKET_PRIVATE_SERVER_HOST, BITBUCKET_REPO_PAT) - + " }", - Arrays.stream(extra).collect(Collectors.joining("\n"))); + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s', 'commit', '%s').customDomainHttps('%s')" + + ".serverAuth('%s');", BITBUCKET_REPO_PAT_REPO_ORG, tag, BITBUCKET_PRIVATE_SERVER_HOST, BITBUCKET_REPO_PAT) + + " }", + Arrays.stream(extra).collect(Collectors.joining("\n"))); } @Test @@ -92,8 +93,8 @@ public void gitlabAuthTag() throws IOException { public void bitbucketCloudAuth() throws IOException { settingsBitbucketBasicAuth("master"); write("build.gradle", - "apply plugin: 'com.diffplug.blowdryer'", - "assert 干.file('sample').text == 'a'"); + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'a'"); gradleRunner().build(); } @@ -101,8 +102,8 @@ public void bitbucketCloudAuth() throws IOException { public void bitbucketServerAuth() throws IOException { settingsBitbucketPersonalAccessTokenAuth("master"); write("build.gradle", - "apply plugin: 'com.diffplug.blowdryer'", - "assert 干.file('checkstyle/spotless.gradle').text == 'a'"); + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('checkstyle/spotless.gradle').text == 'a'"); gradleRunner().build(); } diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java index 5699ec7..6f16a5b 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java @@ -59,24 +59,24 @@ private void settingsGitlabRootFolder(String tag, String... extra) throws IOExce private void settingsBitbucket(String tag, String... extra) throws IOException { write(SETTINGS_GRADLE, - "plugins { id 'com.diffplug.blowdryerSetup' }", - String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s') }", BITBUCKET_REPO_ORG, tag), - Arrays.stream(extra).collect(Collectors.joining("\n"))); + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s') }", BITBUCKET_REPO_ORG, tag), + Arrays.stream(extra).collect(Collectors.joining("\n"))); } private void settingsCustomBitbucket(String tag, String... extra) throws IOException { write(SETTINGS_GRADLE, - "plugins { id 'com.diffplug.blowdryerSetup' }", - String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s').customDomainHttps('api.bitbucket.org/2.0/repositories') }", - BITBUCKET_REPO_ORG, tag), - Arrays.stream(extra).collect(Collectors.joining("\n"))); + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s').customDomainHttps('api.bitbucket.org/2.0/repositories') }", + BITBUCKET_REPO_ORG, tag), + Arrays.stream(extra).collect(Collectors.joining("\n"))); } private void settingsCustomBitbucket(String tag, int host, String... extra) throws IOException { write(SETTINGS_GRADLE, - "plugins { id 'com.diffplug.blowdryerSetup' }", - String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s').customDomainHttps('%s') }", BITBUCKET_REPO_ORG, tag, host), - Arrays.stream(extra).collect(Collectors.joining("\n"))); + "plugins { id 'com.diffplug.blowdryerSetup' }", + String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s').customDomainHttps('%s') }", BITBUCKET_REPO_ORG, tag, host), + Arrays.stream(extra).collect(Collectors.joining("\n"))); } private void settingsLocalJar(String dependency) throws IOException { @@ -141,25 +141,25 @@ public void gitlabTag() throws IOException { public void bitbucketTag() throws IOException { settingsBitbucket("test/2/a"); write(BUILD_GRADLE, - "apply plugin: 'com.diffplug.blowdryer'", - "assert 干.file('sample').text == 'a'", - "assert 干.prop('sample', 'name') == 'test'", - "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'a'", + "assert 干.prop('sample', 'name') == 'test'", + "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); gradleRunner().build(); settingsBitbucket("test/2/b"); write(BUILD_GRADLE, - "apply plugin: 'com.diffplug.blowdryer'", - "assert 干.file('sample').text == 'b'", - "assert 干.prop('sample', 'name') == 'testB'", - "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'b'", + "assert 干.prop('sample', 'name') == 'testB'", + "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); gradleRunner().build(); // double-check that failures do fail settingsBitbucket("test/2/b"); write(BUILD_GRADLE, - "plugins { id 'com.diffplug.blowdryer' }", - "assert Blowdryer.file('sample').text == 'a'"); + "plugins { id 'com.diffplug.blowdryer' }", + "assert Blowdryer.file('sample').text == 'a'"); gradleRunner().buildAndFail(); } @@ -193,32 +193,32 @@ public void customGitlabTag() throws IOException { public void customBitbucketTag() throws IOException { settingsCustomBitbucket("test/2/a"); write(BUILD_GRADLE, - "apply plugin: 'com.diffplug.blowdryer'", - "assert 干.file('sample').text == 'a'", - "assert 干.prop('sample', 'name') == 'test'", - "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'a'", + "assert 干.prop('sample', 'name') == 'test'", + "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); gradleRunner().build(); settingsCustomBitbucket("test/2/b"); write(BUILD_GRADLE, - "apply plugin: 'com.diffplug.blowdryer'", - "assert 干.file('sample').text == 'b'", - "assert 干.prop('sample', 'name') == 'testB'", - "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); + "apply plugin: 'com.diffplug.blowdryer'", + "assert 干.file('sample').text == 'b'", + "assert 干.prop('sample', 'name') == 'testB'", + "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); gradleRunner().build(); // double-check that failures do fail settingsCustomBitbucket("test/2/b"); write(BUILD_GRADLE, - "plugins { id 'com.diffplug.blowdryer' }", - "assert Blowdryer.file('sample').text == 'a'"); + "plugins { id 'com.diffplug.blowdryer' }", + "assert Blowdryer.file('sample').text == 'a'"); gradleRunner().buildAndFail(); // unresolved host settingsCustomBitbucket("test/2/b", 123); write(BUILD_GRADLE, - "plugins { id 'com.diffplug.blowdryer' }", - "assert Blowdryer.file('sample').text == 'a'"); + "plugins { id 'com.diffplug.blowdryer' }", + "assert Blowdryer.file('sample').text == 'a'"); gradleRunner().buildAndFail(); } diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java index 16d2fe2..45acd06 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java @@ -15,24 +15,22 @@ */ package com.diffplug.blowdryer; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + import com.diffplug.blowdryer.Blowdryer.AuthPlugin; import com.diffplug.blowdryer.Blowdryer.ResourcePlugin; import com.diffplug.blowdryer.BlowdryerSetup.Bitbucket; import com.diffplug.blowdryer.BlowdryerSetup.GitAnchorType; -import okhttp3.Request; -import okhttp3.Request.Builder; -import org.junit.Test; - import java.lang.reflect.Field; import java.util.Base64; import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; - -import static java.nio.charset.StandardCharsets.UTF_8; +import okhttp3.Request; +import okhttp3.Request.Builder; +import org.junit.Test; public class BlowdryerTest { private static final String JAR_FILE_RESOURCE_SEPARATOR = "!/"; @@ -44,9 +42,9 @@ public void filenameSafe() { filenameSafe("https://raw.githubusercontent.com/diffplug/durian-build/07f588e52eb0f31e596eab0228a5df7233a98a14/gradle/spotless/spotless.license.java", "https-raw.githubusercontent.com-diffplug--3vpUTw--14-gradle-spotless-spotless.license.java"); filenameSafe("https://mycompany.bitbucket.com/projects/PRJ/repos/my-repo/raw/src/main/resources/checkstyle/spotless.gradle?at=refs%2Fheads%2Fmaster", - "https-mycompany.bitbucket.com-projects-P--7T3UGg--at-refs-2Fheads-2Fmaster-spotless.gradle"); + "https-mycompany.bitbucket.com-projects-P--7T3UGg--at-refs-2Fheads-2Fmaster-spotless.gradle"); filenameSafe("https://mycompany.bitbucket.com/projects/PRJ/repos/my-repo/raw/src/main/resources/checkstyle/spotless.gradle?at=07f588e52eb0f31e596eab0228a5df7233a98a14", - "https-mycompany.bitbucket.com-projects-P--K+HRow--596eab0228a5df7233a98a14-spotless.gradle"); + "https-mycompany.bitbucket.com-projects-P--K+HRow--596eab0228a5df7233a98a14-spotless.gradle"); } private void filenameSafe(String url, String safe) { @@ -95,8 +93,8 @@ public void bitbucketCloud_treeAnchorType() throws Exception { final ResourcePlugin target = getResourcePlugin(); assertThatThrownBy(() -> target.toImmutableUrl("test.properties")) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessage("TREE hash resolution is not supported."); + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage("TREE hash resolution is not supported."); } @Test @@ -123,14 +121,14 @@ public void bitbucketServer_treeAnchorType() throws Exception { final ResourcePlugin target = getResourcePlugin(); assertThatThrownBy(() -> target.toImmutableUrl("test.properties")) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessage("TREE hash resolution is not supported."); + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage("TREE hash resolution is not supported."); } @Test public void bitbucketCloudAuth() throws Exception { final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/testAnchor/src/main/resources/test.properties"; - final String usernameAndAppPassword = String.format("%s:%s", randomUUID(), randomUUID()); + final String usernameAndAppPassword = String.format("%s:%s", randomUUID(), randomUUID()); setupBitbucketTestTarget(GitAnchorType.COMMIT).cloudAuth(usernameAndAppPassword); final ResourcePlugin target = getResourcePlugin(); @@ -141,7 +139,7 @@ public void bitbucketCloudAuth() throws Exception { assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); final String encoded = Base64.getEncoder().encodeToString((usernameAndAppPassword) - .getBytes(UTF_8)); + .getBytes(UTF_8)); assertThat(request.header("Authorization")).isEqualTo(String.format("Basic %s", encoded)); } diff --git a/src/test/java/com/diffplug/blowdryer/GradleHarness.java b/src/test/java/com/diffplug/blowdryer/GradleHarness.java index 7401a8c..5da6991 100644 --- a/src/test/java/com/diffplug/blowdryer/GradleHarness.java +++ b/src/test/java/com/diffplug/blowdryer/GradleHarness.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2020 DiffPlug + * Copyright (C) 2018-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 3f3c1265fec696d80f4e63f6bcadef72835554ad Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 09:02:41 -0700 Subject: [PATCH 05/14] Despecialize the auth token. --- .../diffplug/blowdryer/BlowdryerSetup.java | 70 +++++++++---------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java index 192557b..7ee0028 100644 --- a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java +++ b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java @@ -18,6 +18,7 @@ import com.diffplug.common.annotations.VisibleForTesting; import com.diffplug.common.base.Errors; +import com.diffplug.common.base.Unhandled; import com.google.gson.Gson; import groovy.lang.Closure; import java.io.File; @@ -178,6 +179,7 @@ public class Bitbucket { private String anchor; private GitAnchorType anchorType; private BitbucketType bitbucketType; + private @Nullable String auth; private @Nullable String authToken; private String protocol, host; @@ -195,30 +197,13 @@ private Bitbucket(String repoOrg, GitAnchorType anchorType, String anchor, Bitbu customDomainHttps(BITBUCKET_HOST); } - /** - * Only supported for Bitbucket Server 5.5+ - * Format: "personalAccessToken" - */ public Bitbucket server() { this.bitbucketType = BitbucketType.SERVER; return setGlobals(); } - public Bitbucket serverAuth(String personalAccessToken) { - this.authToken = String.format("Bearer %s", personalAccessToken); - this.bitbucketType = BitbucketType.SERVER; - return setGlobals(); - } - - /** - * Only available for Bitbucket Cloud. - * Format: "username:appPassword" - */ - public Bitbucket cloudAuth(String usernameAndAppPassword) { - final String encoding = Base64.getEncoder().encodeToString((usernameAndAppPassword) - .getBytes(StandardCharsets.UTF_8)); - this.authToken = String.format("Basic %s", encoding); - this.bitbucketType = BitbucketType.CLOUD; + public Bitbucket auth(String auth) { + this.auth = auth; return setGlobals(); } @@ -237,9 +222,23 @@ private Bitbucket customProtocolAndDomain(String protocol, String domain) { } private Bitbucket setGlobals() { + if (auth == null) { + authToken = null; + } else { + switch (bitbucketType) { + case SERVER: + authToken = String.format("Bearer %s", auth); + break; + case CLOUD: + String base64 = Base64.getEncoder().encodeToString((auth).getBytes(StandardCharsets.UTF_8)); + authToken = String.format("Basic %s", base64); + break; + default: + throw Unhandled.enumException(bitbucketType); + } + } Blowdryer.setResourcePluginNull(); String urlStart = getUrlStart(); - Blowdryer.setResourcePlugin(resource -> getFullUrl(urlStart, encodeUrlParts(getFullResourcePath(resource))), (url, builder) -> { if (authToken != null) { builder.addHeader("Authorization", authToken); @@ -248,11 +247,10 @@ private Bitbucket setGlobals() { return this; } - // Bitbucket Cloud and Bitbucket Server (premium, company hosted) has different url structures. - // Bitbucket Cloud uses "org/repo" in URLs, where org is your (or someone else's) account name. - // Bitbucket Server uses "projects/PROJECT_KEY/repos/REPO_NAME" in urls. - private String getUrlStart() { + // Bitbucket Cloud and Bitbucket Server (premium, company hosted) has different url structures. + // Bitbucket Cloud uses "org/repo" in URLs, where org is your (or someone else's) account name. + // Bitbucket Server uses "projects/PROJECT_KEY/repos/REPO_NAME" in urls. if (isServer()) { return String.format("%s%s/projects/%s/repos/%s", protocol, host, repoOrg, repoName); } else { @@ -262,9 +260,9 @@ private String getUrlStart() { private String getFullUrl(String urlStart, String filePath) { if (isServer()) { - return String.format("%s/raw/%s?at=%s", urlStart, filePath, encodeUrlPart(getAnchor())); + return String.format("%s/raw/%s?at=%s", urlStart, filePath, encodeUrlPart(getAnchorForServer())); } else { - return String.format("%s/src/%s/%s", urlStart, encodeUrlParts(getAnchorForCloudAsHash()), filePath); + return String.format("%s/src/%s/%s", urlStart, encodeUrlParts(getAnchorForCloud()), filePath); } } @@ -272,27 +270,28 @@ private boolean isServer() { return BitbucketType.SERVER.equals(this.bitbucketType); } - private String getAnchor() { + private String getAnchorForServer() { switch (anchorType) { case COMMIT: return anchor; case TAG: - return String.format("refs/tags/%s", anchor); + return "refs/tags/" + anchor; default: - throw new UnsupportedOperationException(String.format("%s hash resolution is not supported.", anchorType)); + throw new UnsupportedOperationException(anchorType + " not supported for Bitbucket"); } } - private String getAnchorForCloudAsHash() { + private String getAnchorForCloud() { switch (anchorType) { case COMMIT: return anchor; case TAG: + // rewrite the tag into the commit it points to anchor = getCommitHash("refs/tags/"); anchorType = GitAnchorType.COMMIT; return anchor; default: - throw new UnsupportedOperationException("TREE hash resolution is not supported."); + throw new UnsupportedOperationException(anchorType + " not supported for Bitbucket"); } } @@ -306,11 +305,9 @@ private String getCommitHash(String baseRefs) { @VisibleForTesting String getCommitHashFromBitbucket(String requestUrl) { OkHttpClient client = new OkHttpClient.Builder().build(); - Builder requestBuilder = new Builder() - .url(requestUrl); + Builder requestBuilder = new Builder().url(requestUrl); if (authToken != null) { - requestBuilder - .addHeader("Authorization", authToken); + requestBuilder.addHeader("Authorization", authToken); } Request request = requestBuilder.build(); @@ -336,7 +333,6 @@ private String encodeUrlParts(String part) { } private class RefsTarget { - private final Target target; private RefsTarget(Target target) { @@ -344,13 +340,11 @@ private RefsTarget(Target target) { } private class Target { - private final String hash; private Target(String hash) { this.hash = hash; } - } } } From ea7e53744decf5557bdd6e6ae02051775d7f20ac Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 09:55:56 -0700 Subject: [PATCH 06/14] customDomain only makes sense for Bitbucket server, so we can handle that internally. --- .../com/diffplug/blowdryer/BlowdryerSetup.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java index 7ee0028..2a6998b 100644 --- a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java +++ b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java @@ -194,28 +194,24 @@ private Bitbucket(String repoOrg, GitAnchorType anchorType, String anchor, Bitbu this.anchorType = anchorType; this.bitbucketType = bitbucketType; this.anchor = assertNoLeadingOrTrailingSlash(anchor); - customDomainHttps(BITBUCKET_HOST); + customProtocolAndDomain(BitbucketType.CLOUD, HTTPS_PROTOCOL, BITBUCKET_HOST); } - public Bitbucket server() { - this.bitbucketType = BitbucketType.SERVER; - return setGlobals(); - } - - public Bitbucket auth(String auth) { + public Bitbucket authToken(String auth) { this.auth = auth; return setGlobals(); } public Bitbucket customDomainHttp(String domain) { - return customProtocolAndDomain(HTTP_PROTOCOL, domain); + return customProtocolAndDomain(BitbucketType.SERVER, HTTP_PROTOCOL, domain); } public Bitbucket customDomainHttps(String domain) { - return customProtocolAndDomain(HTTPS_PROTOCOL, domain); + return customProtocolAndDomain(BitbucketType.SERVER, HTTPS_PROTOCOL, domain); } - private Bitbucket customProtocolAndDomain(String protocol, String domain) { + private Bitbucket customProtocolAndDomain(BitbucketType type, String protocol, String domain) { + this.bitbucketType = type; this.protocol = protocol; this.host = domain; return setGlobals(); From 2eaefead245435b4f85919809db2936ad82b5134 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 09:56:05 -0700 Subject: [PATCH 07/14] Condense README further. --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f481788..30fde4f 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,8 @@ blowdryerSetup { // or 'commit', '07f588e52eb0f31e596eab0228a5df7233a98a14' // or 'tree', 'a5df7233a98a1407f588e52eb0f31e596eab0228' - // or gitlab('acme/blowdryer-acme', 'tag', 'v1.4.5').customDomainHttp('acme.org').authToken('abc123') - - // Public/Private Bitbucket Cloud repo configuration. Add .cloudAuth only for private repos. - // bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').cloudAuth("username:appPassword") - - // Public/Private Bitbucket Server repo configurations. User .server for public repos. Use .serverAuth for private repos. - // bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').server().customDomainHttps('my.bitbucket.company.domain.com') - // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').serverAuth('personalAccessToken').customDomainHttps('my.bitbucket.company.domain.com') + // or gitlab('acme/blowdryer-acme', 'tag', 'v1.4.5').authToken('abc123').customDomainHttp('acme.org') + // or bitbucket('acme/blowdryer-acme', 'tag', 'v1.4.5').authToken('abc123').customDomainHttps('acme.org') } ``` * Reference on how to create [application password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) From 3a0d9345b301f2420362722cf90b97a273c64193 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 10:30:06 -0700 Subject: [PATCH 08/14] Update tests for the new API. --- .../java/com/diffplug/blowdryer/BlowdryerTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java index 45acd06..92889b6 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java @@ -71,7 +71,7 @@ public void bitbucketCloud_tagAnchorType() throws Exception { final String hash = UUID.randomUUID().toString(); final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/" + hash + "/src/main/resources/test.properties"; - Bitbucket spy = spy(setupBitbucketTestTarget(GitAnchorType.TAG)).cloudAuth("un:pw"); + Bitbucket spy = spy(setupBitbucketTestTarget(GitAnchorType.TAG)).authToken("un:pw"); doReturn(hash).when(spy).getCommitHashFromBitbucket(hashRequestUrl); final ResourcePlugin target = getResourcePlugin(); @@ -94,13 +94,13 @@ public void bitbucketCloud_treeAnchorType() throws Exception { assertThatThrownBy(() -> target.toImmutableUrl("test.properties")) .isInstanceOf(UnsupportedOperationException.class) - .hasMessage("TREE hash resolution is not supported."); + .hasMessage("TREE not supported for Bitbucket"); } @Test public void bitbucketServer_tagAnchorType() throws Exception { final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=refs%2Ftags%2FtestAnchor"; - setupBitbucketTestTarget(GitAnchorType.TAG).server().customDomainHttps("my.bitbucket.com"); + setupBitbucketTestTarget(GitAnchorType.TAG).customDomainHttps("my.bitbucket.com"); final ResourcePlugin target = getResourcePlugin(); assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); @@ -109,7 +109,7 @@ public void bitbucketServer_tagAnchorType() throws Exception { @Test public void bitbucketServer_commitAnchorType() throws Exception { final String expected = "https://my.bitbucket.com/projects/testOrg/repos/testRepo/raw/src/main/resources/test.properties?at=testAnchor"; - setupBitbucketTestTarget(GitAnchorType.COMMIT).server().customDomainHttps("my.bitbucket.com"); + setupBitbucketTestTarget(GitAnchorType.COMMIT).customDomainHttps("my.bitbucket.com"); final ResourcePlugin target = getResourcePlugin(); assertThat(target.toImmutableUrl("test.properties")).isEqualTo(expected); @@ -117,19 +117,19 @@ public void bitbucketServer_commitAnchorType() throws Exception { @Test public void bitbucketServer_treeAnchorType() throws Exception { - setupBitbucketTestTarget(GitAnchorType.TREE).server().customDomainHttps("my.bitbucket.com"); + setupBitbucketTestTarget(GitAnchorType.TREE).customDomainHttps("my.bitbucket.com"); final ResourcePlugin target = getResourcePlugin(); assertThatThrownBy(() -> target.toImmutableUrl("test.properties")) .isInstanceOf(UnsupportedOperationException.class) - .hasMessage("TREE hash resolution is not supported."); + .hasMessage("TREE not supported for Bitbucket"); } @Test public void bitbucketCloudAuth() throws Exception { final String expected = "https://api.bitbucket.org/2.0/repositories/testOrg/testRepo/src/testAnchor/src/main/resources/test.properties"; final String usernameAndAppPassword = String.format("%s:%s", randomUUID(), randomUUID()); - setupBitbucketTestTarget(GitAnchorType.COMMIT).cloudAuth(usernameAndAppPassword); + setupBitbucketTestTarget(GitAnchorType.COMMIT).authToken(usernameAndAppPassword); final ResourcePlugin target = getResourcePlugin(); final AuthPlugin otherTarget = getAuthPlugin(); From 264c1d84fbf4800f59ee72f222f33b89c3f326b2 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 10:30:40 -0700 Subject: [PATCH 09/14] We don't have a server to test BitbucketServer with, and that's okay. --- .../blowdryer/BlowdryerPluginTest.java | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java index 6f16a5b..36f8543 100644 --- a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java +++ b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java @@ -26,8 +26,7 @@ public class BlowdryerPluginTest extends GradleHarness { private static final String SETTINGS_GRADLE = "settings.gradle"; private static final String BUILD_GRADLE = "build.gradle"; - //todo: change to diffplug if created - private static final String BITBUCKET_REPO_ORG = "bHack"; + private static final String BITBUCKET_REPO_ORG = "diffplug"; private void settingsGithub(String tag, String... extra) throws IOException { write(SETTINGS_GRADLE, @@ -64,21 +63,6 @@ private void settingsBitbucket(String tag, String... extra) throws IOException { Arrays.stream(extra).collect(Collectors.joining("\n"))); } - private void settingsCustomBitbucket(String tag, String... extra) throws IOException { - write(SETTINGS_GRADLE, - "plugins { id 'com.diffplug.blowdryerSetup' }", - String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s').customDomainHttps('api.bitbucket.org/2.0/repositories') }", - BITBUCKET_REPO_ORG, tag), - Arrays.stream(extra).collect(Collectors.joining("\n"))); - } - - private void settingsCustomBitbucket(String tag, int host, String... extra) throws IOException { - write(SETTINGS_GRADLE, - "plugins { id 'com.diffplug.blowdryerSetup' }", - String.format("blowdryerSetup { bitbucket('%s/blowdryer', 'tag', '%s').customDomainHttps('%s') }", BITBUCKET_REPO_ORG, tag, host), - Arrays.stream(extra).collect(Collectors.joining("\n"))); - } - private void settingsLocalJar(String dependency) throws IOException { write(SETTINGS_GRADLE, "plugins { id 'com.diffplug.blowdryerSetup' }", @@ -189,39 +173,6 @@ public void customGitlabTag() throws IOException { gradleRunner().buildAndFail(); } - @Test - public void customBitbucketTag() throws IOException { - settingsCustomBitbucket("test/2/a"); - write(BUILD_GRADLE, - "apply plugin: 'com.diffplug.blowdryer'", - "assert 干.file('sample').text == 'a'", - "assert 干.prop('sample', 'name') == 'test'", - "assert 干.prop('sample', 'ver_spotless') == '1.2.0'"); - gradleRunner().build(); - - settingsCustomBitbucket("test/2/b"); - write(BUILD_GRADLE, - "apply plugin: 'com.diffplug.blowdryer'", - "assert 干.file('sample').text == 'b'", - "assert 干.prop('sample', 'name') == 'testB'", - "assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'"); - gradleRunner().build(); - - // double-check that failures do fail - settingsCustomBitbucket("test/2/b"); - write(BUILD_GRADLE, - "plugins { id 'com.diffplug.blowdryer' }", - "assert Blowdryer.file('sample').text == 'a'"); - gradleRunner().buildAndFail(); - - // unresolved host - settingsCustomBitbucket("test/2/b", 123); - write(BUILD_GRADLE, - "plugins { id 'com.diffplug.blowdryer' }", - "assert Blowdryer.file('sample').text == 'a'"); - gradleRunner().buildAndFail(); - } - @Test public void rootfolderGitlabTag() throws IOException { settingsGitlabRootFolder("test/2/a"); From f460c9ba0e6aeef90e49759a6e5390c6c90b952e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 10:47:42 -0700 Subject: [PATCH 10/14] Minor improvement to detectAndRewriteBitbucketUrlIfRequired. --- src/main/java/com/diffplug/blowdryer/Blowdryer.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/diffplug/blowdryer/Blowdryer.java b/src/main/java/com/diffplug/blowdryer/Blowdryer.java index baf0067..cbac5b7 100644 --- a/src/main/java/com/diffplug/blowdryer/Blowdryer.java +++ b/src/main/java/com/diffplug/blowdryer/Blowdryer.java @@ -191,7 +191,7 @@ private static void downloadRemote(String url, File dst) throws IOException { /** Returns either the filename safe URL, or (first40)--(Base64 filenamesafe)(last40). */ static String filenameSafe(String url) { - url = detectAndRewriteBitbucketUrlIfRequired(url); + url = preserveFileExtensionBitbucket(url); String allSafeCharacters = url.replaceAll("[^a-zA-Z0-9-+_.]", "-"); String noDuplicateDash = allSafeCharacters.replaceAll("-+", "-"); if (noDuplicateDash.length() <= MAX_FILE_LENGTH) { @@ -214,9 +214,10 @@ static String filenameSafe(String url) { // required to retrieve XML files. // From: https://mycompany.bitbucket.com/projects/PRJ/repos/my-repo/raw/src/main/resources/checkstyle/spotless.gradle?at=07f588e52eb0f31e596eab0228a5df7233a98a14 // To: https://mycompany.bitbucket.com/projects/PRJ/repos/my-repo/raw/src/main/resources/checkstyle/spotless.gradle?at=07f588e52eb0f31e596eab0228a5df7233a98a14-spotless.gradle - private static String detectAndRewriteBitbucketUrlIfRequired(String url) { - if (url.contains("?at")) { - String fileNameWithoutQuery = url.substring(0, url.indexOf("?at")); + private static String preserveFileExtensionBitbucket(String url) { + int atIdx = url.indexOf("?at="); + if (atIdx != -1) { + String fileNameWithoutQuery = url.substring(0, atIdx); url = String.format("%s-%s", url, fileNameWithoutQuery.substring(fileNameWithoutQuery.lastIndexOf("/") + 1)); } return url; From 139c186de1ca88ffc9ef87de689a636d88a1636b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 11:04:35 -0700 Subject: [PATCH 11/14] Mockito only needed at test time. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 43bfabc..0455b69 100644 --- a/build.gradle +++ b/build.gradle @@ -19,9 +19,9 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.2.2' implementation 'com.squareup.okio:okio:2.4.1' implementation 'com.google.code.gson:gson:2.8.7' - implementation 'org.mockito:mockito-core:3.11.2' implementation 'com.diffplug.durian:durian-core:1.2.0' implementation 'com.diffplug.durian:durian-io:1.2.0' testImplementation 'junit:junit:4.12' testImplementation 'org.assertj:assertj-core:3.14.0' + testImplementation 'org.mockito:mockito-core:3.11.2' } From dcc36dd4f6f2cfeab798b7c53282b48af0bddae8 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 11:04:42 -0700 Subject: [PATCH 12/14] Unnecessary change. --- src/test/java/com/diffplug/blowdryer/GradleHarness.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/com/diffplug/blowdryer/GradleHarness.java b/src/test/java/com/diffplug/blowdryer/GradleHarness.java index 5da6991..b37600c 100644 --- a/src/test/java/com/diffplug/blowdryer/GradleHarness.java +++ b/src/test/java/com/diffplug/blowdryer/GradleHarness.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2021 DiffPlug + * Copyright (C) 2018-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,5 +24,4 @@ public class GradleHarness extends ResourceHarness { protected GradleRunner gradleRunner() throws IOException { return GradleRunner.create().withProjectDir(rootFolder()).withPluginClasspath(); } - } From 387cf245a72b089d11cec639a84add6a832aa6f0 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 11:05:49 -0700 Subject: [PATCH 13/14] Simplify changelog. --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 136e69d..c6c9fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- `filenameSafe` now, if required, removes query parameters (for Bitbucket) and appends file name to the end of the link to preserve its original file extension. - Support for Bitbucket Cloud and Server ([#23](https://github.com/diffplug/blowdryer/pull/23)). ## [1.2.1] - 2021-06-01 From 8d3bc3d77281a9341ef95f54593525ae2cd10134 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 23 Jun 2021 11:24:50 -0700 Subject: [PATCH 14/14] Improve error message for surprising metaFile content. --- src/main/java/com/diffplug/blowdryer/Blowdryer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/diffplug/blowdryer/Blowdryer.java b/src/main/java/com/diffplug/blowdryer/Blowdryer.java index cbac5b7..ee7c615 100644 --- a/src/main/java/com/diffplug/blowdryer/Blowdryer.java +++ b/src/main/java/com/diffplug/blowdryer/Blowdryer.java @@ -101,11 +101,14 @@ public static File immutableUrl(String url) { if (metaFile.exists() && dataFile.exists()) { Map props = loadPropertyFile(metaFile); String propUrl = props.get(PROP_URL); + if (propUrl == null) { + throw new IllegalArgumentException("Unexpected content, recommend deleting file at " + metaFile); + } if (propUrl.equals(url)) { urlToContent.put(url, dataFile); return dataFile; } else { - throw new IllegalStateException("Expected url " + url + " but was " + propUrl + " for " + metaFile.getAbsolutePath()); + throw new IllegalStateException("Expected url " + url + " but was " + propUrl + ", recommend deleting file at " + metaFile.getAbsolutePath()); } } else { Files.createParentDirs(dataFile);