diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e04c0d..c6c9fd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+- Support for Bitbucket Cloud and Server ([#23](https://github.com/diffplug/blowdryer/pull/23)).
+
## [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..30fde4f 100644
--- a/README.md
+++ b/README.md
@@ -54,9 +54,15 @@ 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')
+
+ // 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/)
+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:
diff --git a/build.gradle b/build.gradle
index bfd1863..0455b69 100644
--- a/build.gradle
+++ b/build.gradle
@@ -18,8 +18,10 @@ 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'
testImplementation 'org.assertj:assertj-core:3.14.0'
+ testImplementation 'org.mockito:mockito-core:3.11.2'
}
diff --git a/src/main/java/com/diffplug/blowdryer/Blowdryer.java b/src/main/java/com/diffplug/blowdryer/Blowdryer.java
index 1bdc949..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);
@@ -191,6 +194,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 = preserveFileExtensionBitbucket(url);
String allSafeCharacters = url.replaceAll("[^a-zA-Z0-9-+_.]", "-");
String noDuplicateDash = allSafeCharacters.replaceAll("-+", "-");
if (noDuplicateDash.length() <= MAX_FILE_LENGTH) {
@@ -209,6 +213,19 @@ 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 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;
+ }
+
//////////////////////
// plugin interface //
//////////////////////
@@ -253,7 +270,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..2a6998b 100644
--- a/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java
+++ b/src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java
@@ -16,14 +16,26 @@
package com.diffplug.blowdryer;
+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;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
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 +44,10 @@ 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";
+
+ private static final String HTTP_PROTOCOL = "http://";
+ private static final String HTTPS_PROTOCOL = "https://";
private final File referenceDirectory;
@@ -46,7 +62,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
@@ -86,7 +102,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);
@@ -121,11 +137,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 +163,188 @@ 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 String repoOrg;
+ private String repoName;
+ private String anchor;
+ private GitAnchorType anchorType;
+ private BitbucketType bitbucketType;
+ private @Nullable String auth;
+ 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);
+ customProtocolAndDomain(BitbucketType.CLOUD, HTTPS_PROTOCOL, BITBUCKET_HOST);
+ }
+
+ public Bitbucket authToken(String auth) {
+ this.auth = auth;
+ return setGlobals();
+ }
+
+ public Bitbucket customDomainHttp(String domain) {
+ return customProtocolAndDomain(BitbucketType.SERVER, HTTP_PROTOCOL, domain);
+ }
+
+ public Bitbucket customDomainHttps(String domain) {
+ return customProtocolAndDomain(BitbucketType.SERVER, HTTPS_PROTOCOL, domain);
+ }
+
+ private Bitbucket customProtocolAndDomain(BitbucketType type, String protocol, String domain) {
+ this.bitbucketType = type;
+ this.protocol = protocol;
+ this.host = domain;
+ return setGlobals();
+ }
+
+ 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);
+ }
+ });
+ return this;
+ }
+
+ 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 {
+ 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(getAnchorForServer()));
+ } else {
+ return String.format("%s/src/%s/%s", urlStart, encodeUrlParts(getAnchorForCloud()), filePath);
+ }
+ }
+
+ private boolean isServer() {
+ return BitbucketType.SERVER.equals(this.bitbucketType);
+ }
+
+ private String getAnchorForServer() {
+ switch (anchorType) {
+ case COMMIT:
+ return anchor;
+ case TAG:
+ return "refs/tags/" + anchor;
+ default:
+ throw new UnsupportedOperationException(anchorType + " not supported for Bitbucket");
+ }
+ }
+
+ 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(anchorType + " not supported for Bitbucket");
+ }
+ }
+
+ // 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);
+ }
+
+ @VisibleForTesting
+ 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", 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;
+
+ 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 +412,5 @@ private static String encodeUrlPart(String part) {
throw new IllegalArgumentException("error encoding part", e);
}
}
+
}
diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java
index 1829d8a..4995356 100644
--- a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java
+++ b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java
@@ -25,9 +25,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 +53,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', '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")));
+ }
+
@Test
public void githubAuthTag() throws IOException {
settingsGithubAuth("master");
@@ -63,6 +89,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..36f8543 100644
--- a/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java
+++ b/src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java
@@ -26,6 +26,7 @@ public class BlowdryerPluginTest extends GradleHarness {
private static final String SETTINGS_GRADLE = "settings.gradle";
private static final String BUILD_GRADLE = "build.gradle";
+ private static final String BITBUCKET_REPO_ORG = "diffplug";
private void settingsGithub(String tag, String... extra) throws IOException {
write(SETTINGS_GRADLE,
@@ -55,6 +56,13 @@ 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 settingsLocalJar(String dependency) throws IOException {
write(SETTINGS_GRADLE,
"plugins { id 'com.diffplug.blowdryerSetup' }",
@@ -113,6 +121,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");
diff --git a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java
index 3d126d6..92889b6 100644
--- a/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java
+++ b/src/test/java/com/diffplug/blowdryer/BlowdryerTest.java
@@ -15,8 +15,21 @@
*/
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 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 java.lang.reflect.Field;
+import java.util.Base64;
+import java.util.UUID;
+import okhttp3.Request;
+import okhttp3.Request.Builder;
import org.junit.Test;
public class BlowdryerTest {
@@ -28,23 +41,128 @@ 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://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) {
- 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 spy = spy(setupBitbucketTestTarget(GitAnchorType.TAG)).authToken("un:pw");
+ doReturn(hash).when(spy).getCommitHashFromBitbucket(hashRequestUrl);
+ 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_treeAnchorType() throws Exception {
+ setupBitbucketTestTarget(GitAnchorType.TREE);
+ final ResourcePlugin target = getResourcePlugin();
+
+ assertThatThrownBy(() -> target.toImmutableUrl("test.properties"))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .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).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).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).customDomainHttps("my.bitbucket.com");
+ final ResourcePlugin target = getResourcePlugin();
+
+ assertThatThrownBy(() -> target.toImmutableUrl("test.properties"))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .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).authToken(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();
+ }
+
}