diff --git a/.editorconfig b/.editorconfig
index bcafffd..ef924dc 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,3 +1,9 @@
-[*.java]
+root = true
+
+[{*.java,*.xml}]
indent_style = tab
indent_size = 4
+
+[{*.css,*.js,*.soy,*.yml}]
+indent_style = space
+indent_size = 2
diff --git a/.gitignore b/.gitignore
index f706c22..bafd62c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
.project
.settings
-.vscode
+.classpath
target
tmp
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..de456f1
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,17 @@
+dist: trusty
+language: java
+jdk:
+ - openjdk8
+before_install:
+ - wget https://marketplace.atlassian.com/download/plugins/atlassian-plugin-sdk-tgz
+ - mkdir opt
+ - tar -xvzf *plugin-sdk* -C opt
+ - mv opt/*plugin-sdk* opt/atlassian-plugin-sdk
+ - chmod a+x opt/atlassian-plugin-sdk/bin/*
+ - chmod a+x opt/atlassian-plugin-sdk/apache-maven-*/bin/*
+ - export PATH=opt/atlassian-plugin-sdk/bin:opt/atlassian-plugin-sdk/apache-maven-*/bin:$PATH
+ - atlas-version
+install:
+ - atlas-mvn install -DskipTests=true
+script:
+ - atlas-package
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f7527fc
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "java.configuration.updateBuildConfiguration": "automatic",
+ "maven.executable.path": "/usr/local/Cellar/atlassian-plugin-sdk/8.0.16/libexec/apache-maven-3.5.4/bin/mvn",
+ "java.configuration.maven.userSettings": "/usr/local/Cellar/atlassian-plugin-sdk/8.0.16/libexec/apache-maven-3.5.4/conf/settings.xml"
+}
diff --git a/README b/README.md
similarity index 83%
rename from README
rename to README.md
index f750230..f948985 100644
--- a/README
+++ b/README.md
@@ -20,3 +20,10 @@ Useful resources:
* [SAL 3.0 Upgrade Guide](https://developer.atlassian.com/server/framework/atlassian-sdk/sal-3-0-upgrade-guide/)
* [Atlassian Community - REST requests from a custom plugin](https://community.atlassian.com/t5/Answers-Developer-Questions/What-is-the-recommended-way-to-send-REST-requests-from-a-custom/qaq-p/568227)
* [Atlassian AUI - Soy template components](https://bitbucket.org/atlassian/aui/src/6a8e37f85a07952cc290503f54d85ff5bebc8783/src/soy/?at=5.10.x)
+
+Code samples:
+
+* https://bitbucket.org/atlassian/stash-git-ops-plugin/src/master/
+* https://bitbucket.org/atlassianlabs/stash-refchange-settings-plugin/src/master/
+* https://bitbucket.org/atlassian/bitbucket-code-coverage/src/master/code-coverage-plugin/
+* https://bitbucket.org/atlassian/stash-auto-unapprove-plugin/src/master/
diff --git a/pom.xml b/pom.xml
index 77c3f50..bf8992e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,198 +1,221 @@
- 4.0.0
-
- com.recuencojones.bitbucket
- log-on-clone
- 1.0.0-SNAPSHOT
- atlassian-plugin
-
- log-on-clone
- This is the com.recuencojones.bitbucket:log-on-clone plugin for Atlassian Bitbucket Server.
-
-
- Example Company
- http://www.example.com/
-
-
-
- true
- 1.8
- 1.8
- UTF-8
-
- 8.0.2
- 5.16.0
- ${bitbucket.version}
-
-
- ${project.groupId}.${project.artifactId}
-
- 2.1.7
- 1
- 1.1.1
- 2.0.1
-
-
-
-
-
- com.atlassian.bitbucket.server
- bitbucket-parent
- ${bitbucket.version}
- pom
- import
-
-
-
-
-
- com.atlassian.bitbucket.server
- bitbucket-api
- provided
-
-
- com.atlassian.bitbucket.server
- bitbucket-spi
- provided
-
-
- com.atlassian.soy
- soy-template-renderer-api
- provided
-
-
- com.atlassian.plugins
- atlassian-plugins-webresource
- provided
-
-
- com.atlassian.plugins.rest
- atlassian-rest-common
- provided
-
-
- com.atlassian.plugins.rest
- atlassian-rest-module
- provided
-
-
- com.atlassian.bitbucket.server
- bitbucket-rest-model
- provided
-
-
-
- com.atlassian.plugin
- atlassian-spring-scanner-annotation
- ${atlassian.spring.scanner.version}
- provided
-
-
- com.atlassian.sal
- sal-api
- provided
-
-
-
- com.google.code.gson
- gson
- provided
-
-
- javax.inject
- javax.inject
- ${javax.inject.version}
- provided
-
-
- javax.servlet
- javax.servlet-api
- provided
-
-
- javax.ws.rs
- jsr311-api
- ${jsr311.version}
- provided
-
-
- org.apache.commons
- commons-lang3
- provided
-
-
-
- com.atlassian.plugins
- atlassian-plugins-osgi-testrunner
- ${plugin.testrunner.version}
- test
-
-
- junit
- junit
- test
-
-
-
-
-
-
- com.atlassian.maven.plugins
- bitbucket-maven-plugin
- ${amps.version}
- true
-
-
-
- bitbucket
- bitbucket
- ${bitbucket.version}
- ${bitbucket.data.version}
-
-
-
- ${atlassian.plugin.key}
-
-
-
- com.recuencojones.bitbucket.log.api,
-
-
-
-
- org.springframework.osgi.*;resolution:="optional",
- org.eclipse.gemini.blueprint.*;resolution:="optional",
- *
-
-
-
- *
-
-
-
-
- com.atlassian.plugin
- atlassian-spring-scanner-maven-plugin
- ${atlassian.spring.scanner.version}
-
-
-
- atlassian-spring-scanner
-
- process-classes
-
-
-
-
-
- com.atlassian.plugin
- atlassian-spring-scanner-external-jar
-
-
- false
-
-
-
-
+ 4.0.0
+
+ com.recuencojones.bitbucket
+ log-on-clone
+ 1.0.0-SNAPSHOT
+ atlassian-plugin
+
+ log-on-clone
+ This is the com.recuencojones.bitbucket:log-on-clone plugin for Atlassian Bitbucket Server.
+
+
+ Example Company
+ http://www.example.com/
+
+
+
+ true
+ 1.8
+ 1.8
+ UTF-8
+
+ 8.0.2
+ 5.16.0
+ ${bitbucket.version}
+
+
+ ${project.groupId}.${project.artifactId}
+
+ 2.1.7
+ 1
+ 1.1.1
+ 2.0.1
+
+
+
+
+
+ com.atlassian.bitbucket.server
+ bitbucket-parent
+ ${bitbucket.version}
+ pom
+ import
+
+
+
+
+
+ com.atlassian.bitbucket.server
+ bitbucket-api
+ provided
+
+
+ com.atlassian.bitbucket.server
+ bitbucket-spi
+ provided
+
+
+ com.atlassian.activeobjects
+ activeobjects-plugin
+ provided
+
+
+ com.atlassian.soy
+ soy-template-renderer-api
+ provided
+
+
+ com.atlassian.plugins
+ atlassian-plugins-webresource
+ provided
+
+
+ com.atlassian.plugins.rest
+ atlassian-rest-common
+ provided
+
+
+ com.atlassian.plugins.rest
+ atlassian-rest-module
+ provided
+
+
+ com.atlassian.bitbucket.server
+ bitbucket-rest-model
+ provided
+
+
+
+ org.springframework
+ spring-beans
+ provided
+
+
+ org.springframework
+ spring-context
+ provided
+
+
+
+ com.atlassian.plugin
+ atlassian-spring-scanner-annotation
+ ${atlassian.spring.scanner.version}
+ provided
+
+
+ com.atlassian.sal
+ sal-api
+ provided
+
+
+
+ com.google.code.gson
+ gson
+ provided
+
+
+ javax.inject
+ javax.inject
+ ${javax.inject.version}
+ provided
+
+
+ javax.servlet
+ javax.servlet-api
+ provided
+
+
+ javax.ws.rs
+ jsr311-api
+ ${jsr311.version}
+ provided
+
+
+ org.apache.commons
+ commons-lang3
+ provided
+
+
+
+ com.atlassian.plugins
+ atlassian-plugins-osgi-testrunner
+ ${plugin.testrunner.version}
+ test
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
+
+
+ com.atlassian.maven.plugins
+ bitbucket-maven-plugin
+ ${amps.version}
+ true
+
+
+
+ bitbucket
+ bitbucket
+ ${bitbucket.version}
+ ${bitbucket.data.version}
+
+
+
+ ${atlassian.plugin.key}
+
+
+
+ com.recuencojones.bitbucket.log,
+ com.recuencojones.bitbucket.log.dao,
+ com.recuencojones.bitbucket.log.api,
+
+
+
+
+ org.springframework.osgi.*;resolution:="optional",
+ org.eclipse.gemini.blueprint.*;resolution:="optional",
+ *
+
+
+
+ *
+
+
+
+
+ com.atlassian.plugin
+ atlassian-spring-scanner-maven-plugin
+ ${atlassian.spring.scanner.version}
+
+
+
+ atlassian-spring-scanner
+
+ process-classes
+
+
+
+
+
+ com.atlassian.plugin
+ atlassian-spring-scanner-external-jar
+
+
+ false
+
+
+
+
diff --git a/src/main/java/com/recuencojones/bitbucket/log/OnRepositoryClone.java b/src/main/java/com/recuencojones/bitbucket/log/OnRepositoryClone.java
index 18843d6..f8a851a 100644
--- a/src/main/java/com/recuencojones/bitbucket/log/OnRepositoryClone.java
+++ b/src/main/java/com/recuencojones/bitbucket/log/OnRepositoryClone.java
@@ -1,57 +1,61 @@
package com.recuencojones.bitbucket.log;
+import com.recuencojones.bitbucket.log.dao.*;
+
import com.atlassian.bitbucket.event.repository.RepositoryCloneEvent;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.event.api.EventListener;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import com.atlassian.sal.api.net.*;
-import com.atlassian.sal.api.pluginsettings.PluginSettings;
-import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.inject.Named;
import javax.inject.Inject;
+import javax.inject.Named;
-@Named("onRepositoryClone")
+@Named
public class OnRepositoryClone {
private static final Logger log = LoggerFactory.getLogger(OnRepositoryClone.class);
@ComponentImport
- private final RequestFactory requestFactory;
+ private final RequestFactory> requestFactory;
- @ComponentImport
- private final PluginSettingsFactory pluginSettingsFactory;
+ private final RepositoryCloneSettingsDAO repositoryCloneSettingsDAO;
@Inject
public OnRepositoryClone(
- final RequestFactory requestFactory,
- final PluginSettingsFactory pluginSettingsFactory
+ final RequestFactory> requestFactory,
+ final RepositoryCloneSettingsDAO repositoryCloneSettingsDAO
) {
this.requestFactory = requestFactory;
- this.pluginSettingsFactory = pluginSettingsFactory;
+ this.repositoryCloneSettingsDAO = repositoryCloneSettingsDAO;
}
@EventListener
- public void onCloneEvent(final RepositoryCloneEvent cloneEvent) {
- final Repository repository = cloneEvent.getRepository();
- final String projectKey = repository.getProject().getKey();
+ public void onCloneEvent(final RepositoryCloneEvent event) {
+ final Repository repository = event.getRepository();
+ final int repositoryID = repository.getId();
final String repositorySlug = repository.getSlug();
+ final String projectKey = repository.getProject().getKey();
+
+ final RepositoryCloneSettings settings = repositoryCloneSettingsDAO.get(repositoryID);
- log.info("Repository {}/{} cloned", projectKey, repositorySlug);
+ if (settings != null && settings.isEnabled()) {
+ log.debug("Repository {}/{} has log-on-clone settings", projectKey, repositorySlug);
- final Request request = requestFactory.createRequest(Request.MethodType.POST, "https://en6qhxx7a3ksl.x.pipedream.net");
+ final Request, ?> request = requestFactory.createRequest(Request.MethodType.POST, settings.getURL());
- request.setRequestBody(new Gson().toJson(repository));
+ request.setRequestBody(new Gson().toJson(repository));
- try {
- request.execute();
- } catch (final ResponseException e) {
- log.error("Could not log clone of {}/{}. Skipping.", projectKey, repositorySlug);
+ try {
+ request.execute();
+ } catch (final ResponseException e) {
+ log.error("Could not log clone of {}/{}. Skipping.", projectKey, repositorySlug);
+ }
}
}
}
diff --git a/src/main/java/com/recuencojones/bitbucket/log/OnRepositoryDeleted.java b/src/main/java/com/recuencojones/bitbucket/log/OnRepositoryDeleted.java
index a564186..910db8c 100644
--- a/src/main/java/com/recuencojones/bitbucket/log/OnRepositoryDeleted.java
+++ b/src/main/java/com/recuencojones/bitbucket/log/OnRepositoryDeleted.java
@@ -1,42 +1,43 @@
package com.recuencojones.bitbucket.log;
+import com.recuencojones.bitbucket.log.dao.*;
+
import com.atlassian.bitbucket.event.repository.RepositoryDeletedEvent;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.event.api.EventListener;
-import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
-
-import com.atlassian.sal.api.pluginsettings.PluginSettings;
-import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javax.inject.Inject;
-@Named("onRepositoryDeleted")
+@Named
public class OnRepositoryDeleted {
private static final Logger log = LoggerFactory.getLogger(OnRepositoryClone.class);
- @ComponentImport
- private final PluginSettingsFactory pluginSettingsFactory;
+ private final RepositoryCloneSettingsDAO repositoryCloneSettingsDAO;
@Inject
public OnRepositoryDeleted(
- final PluginSettingsFactory pluginSettingsFactory
+ final RepositoryCloneSettingsDAO repositoryCloneSettingsDAO
) {
- this.pluginSettingsFactory = pluginSettingsFactory;
+ this.repositoryCloneSettingsDAO = repositoryCloneSettingsDAO;
}
@EventListener
public void onRepositoryDeleted(final RepositoryDeletedEvent event) {
- deleteSettings(event.getRepository());
- }
-
- private void deleteSettings(final Repository repository) {
-
+ final Repository repository = event.getRepository();
+ final int repositoryID = repository.getId();
+ final String repositorySlug = repository.getSlug();
+ final String projectKey = repository.getProject().getKey();
+ final RepositoryCloneSettings settings = repositoryCloneSettingsDAO.get(repositoryID);
+
+ if (settings != null) {
+ log.debug("Repository {}/{} has log-on-clone settings, delete.", projectKey, repositorySlug);
+ repositoryCloneSettingsDAO.remove(settings);
+ }
}
}
diff --git a/src/main/java/com/recuencojones/bitbucket/log/RepositoryCloneSettingsServlet.java b/src/main/java/com/recuencojones/bitbucket/log/RepositoryCloneSettingsServlet.java
index f28d84c..1042031 100644
--- a/src/main/java/com/recuencojones/bitbucket/log/RepositoryCloneSettingsServlet.java
+++ b/src/main/java/com/recuencojones/bitbucket/log/RepositoryCloneSettingsServlet.java
@@ -5,9 +5,6 @@
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
-import com.atlassian.sal.api.pluginsettings.PluginSettings;
-import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
-
import com.atlassian.soy.renderer.SoyException;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.atlassian.webresource.api.assembler.PageBuilderService;
@@ -15,9 +12,6 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import javax.inject.Inject;
import javax.servlet.ServletException;
@@ -29,11 +23,9 @@
import java.util.Map;
-class RepositoryCloneSettingsServlet extends HttpServlet {
- private static final Logger log = LoggerFactory.getLogger(RepositoryCloneSettingsServlet.class);
+public class RepositoryCloneSettingsServlet extends HttpServlet {
- @ComponentImport
- private final PluginSettingsFactory pluginSettingsFactory;
+ private static final long serialVersionUID = 1L;
@ComponentImport
private final RepositoryService repositoryService;
@@ -46,12 +38,10 @@ class RepositoryCloneSettingsServlet extends HttpServlet {
@Inject
public RepositoryCloneSettingsServlet(
- final PluginSettingsFactory pluginSettingsFactory,
final RepositoryService repositoryService,
final SoyTemplateRenderer soyTemplateRenderer,
final PageBuilderService pageBuilderService
) {
- this.pluginSettingsFactory = pluginSettingsFactory;
this.repositoryService = repositoryService;
this.soyTemplateRenderer = soyTemplateRenderer;
this.pageBuilderService = pageBuilderService;
diff --git a/src/main/java/com/recuencojones/bitbucket/log/dao/RepositoryCloneSettings.java b/src/main/java/com/recuencojones/bitbucket/log/dao/RepositoryCloneSettings.java
new file mode 100644
index 0000000..db240ff
--- /dev/null
+++ b/src/main/java/com/recuencojones/bitbucket/log/dao/RepositoryCloneSettings.java
@@ -0,0 +1,37 @@
+package com.recuencojones.bitbucket.log.dao;
+
+import net.java.ao.Accessor;
+import net.java.ao.Entity;
+import net.java.ao.Mutator;
+import net.java.ao.schema.Indexed;
+import net.java.ao.schema.NotNull;
+import net.java.ao.schema.StringLength;
+import net.java.ao.schema.Table;
+
+import static net.java.ao.schema.StringLength.UNLIMITED;
+
+@Table("RC_SETTINGS")
+public interface RepositoryCloneSettings extends Entity {
+
+ String REPO_ID_COLUMN = "REPO_ID";
+ String ENABLED_COLUMN = "ENABLED";
+ String URL_COLUMN = "URL";
+
+ @Indexed
+ @Accessor(REPO_ID_COLUMN)
+ @NotNull
+ int getId();
+
+ @Accessor(ENABLED_COLUMN)
+ boolean isEnabled();
+
+ @Mutator(ENABLED_COLUMN)
+ void setEnabled(boolean enabled);
+
+ @Accessor(URL_COLUMN)
+ @StringLength(UNLIMITED)
+ String getURL();
+
+ @Mutator(URL_COLUMN)
+ void setURL(String url);
+}
diff --git a/src/main/java/com/recuencojones/bitbucket/log/dao/RepositoryCloneSettingsDAO.java b/src/main/java/com/recuencojones/bitbucket/log/dao/RepositoryCloneSettingsDAO.java
new file mode 100644
index 0000000..5f10a3d
--- /dev/null
+++ b/src/main/java/com/recuencojones/bitbucket/log/dao/RepositoryCloneSettingsDAO.java
@@ -0,0 +1,68 @@
+package com.recuencojones.bitbucket.log.dao;
+
+import com.atlassian.activeobjects.external.ActiveObjects;
+
+import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
+import org.springframework.stereotype.Component;
+
+import net.java.ao.DBParam;
+
+import javax.inject.Inject;
+
+import static com.recuencojones.bitbucket.log.dao.RepositoryCloneSettings.REPO_ID_COLUMN;
+
+@Component
+public class RepositoryCloneSettingsDAO {
+ private static final String ID_QUERY = String.format("%s = ?", REPO_ID_COLUMN);
+
+ @ComponentImport
+ private final ActiveObjects ao;
+
+ @Inject
+ public RepositoryCloneSettingsDAO(
+ ActiveObjects ao
+ ) {
+ this.ao = ao;
+ }
+
+ public RepositoryCloneSettings save(int repositoryID, String url, boolean enabled) {
+ RepositoryCloneSettings settings = find(repositoryID);
+
+ if (settings == null) {
+ settings = ao.create(
+ RepositoryCloneSettings.class,
+ new DBParam(REPO_ID_COLUMN, repositoryID)
+ );
+ }
+
+ settings.setURL(url);
+ settings.setEnabled(enabled);
+ settings.save();
+
+ return settings;
+ }
+
+ public RepositoryCloneSettings get(int repositoryID) {
+ return find(repositoryID);
+ }
+
+ public void remove(int repositoryID) {
+ RepositoryCloneSettings settings = find(repositoryID);
+
+ remove(settings);
+ }
+
+ public void remove(RepositoryCloneSettings settings) {
+ ao.delete(settings);
+ }
+
+ private RepositoryCloneSettings find(int repositoryID) {
+ RepositoryCloneSettings[] results = ao.find(RepositoryCloneSettings.class, ID_QUERY, repositoryID);
+
+ if (results.length == 0) {
+ return null;
+ }
+
+ return results[0];
+ }
+}
diff --git a/src/main/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsDTO.java b/src/main/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsDTO.java
new file mode 100644
index 0000000..24ed133
--- /dev/null
+++ b/src/main/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsDTO.java
@@ -0,0 +1,30 @@
+package com.recuencojones.bitbucket.log.rest;
+
+import org.codehaus.jackson.annotate.JsonIgnoreProperties;
+import org.codehaus.jackson.annotate.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class RepositoryCloneSettingsDTO {
+
+ @JsonProperty("enabled")
+ private boolean enabled;
+
+ @JsonProperty("url")
+ private String url;
+
+ public boolean getEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getURL() {
+ return url;
+ }
+
+ public void setURL(String url) {
+ this.url = url;
+ }
+}
diff --git a/src/main/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsResource.java b/src/main/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsResource.java
index 1231324..a03bbc7 100644
--- a/src/main/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsResource.java
+++ b/src/main/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsResource.java
@@ -1,5 +1,7 @@
package com.recuencojones.bitbucket.log.rest;
+import com.recuencojones.bitbucket.log.dao.*;
+
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.rest.util.ResourcePatterns;
import com.atlassian.bitbucket.rest.util.ResponseFactory;
@@ -34,26 +36,41 @@ public class RepositoryCloneSettingsResource {
@ComponentImport
private final PermissionValidationService permissionValidationService;
+ private final RepositoryCloneSettingsDAO repositoryCloneSettingsDAO;
+
@Inject
public RepositoryCloneSettingsResource(
- PermissionValidationService permissionValidationService
+ PermissionValidationService permissionValidationService,
+ RepositoryCloneSettingsDAO repositoryCloneSettingsDAO
) {
this.permissionValidationService = permissionValidationService;
+ this.repositoryCloneSettingsDAO = repositoryCloneSettingsDAO;
}
@GET
public Response getSettings(@Context Repository repository) {
log.info("Retrieve logging url for repository {}/{}", repository.getProject().getKey(), repository.getSlug());
- // return ResponseFactory.ok(new RestAutoUnapproveSettings(unapproveService.getSettings(scope))).build();
+ RepositoryCloneSettings settings = repositoryCloneSettingsDAO.get(repository.getId());
+
+ if (settings != null) {
+ RepositoryCloneSettingsDTO settingsDTO = new RepositoryCloneSettingsDTO();
+
+ settingsDTO.setEnabled(settings.isEnabled());
+ settingsDTO.setURL(settings.getURL());
+
+ return ResponseFactory.ok(settingsDTO).build();
+ }
+
return ResponseFactory.ok().build();
}
@POST
- public Response saveSettings(@Context Repository repository) {
+ public Response saveSettings(@Context Repository repository, RepositoryCloneSettingsDTO settings) {
permissionValidationService.validateForRepository(repository, Permission.REPO_ADMIN);
log.info("Store logging url for repository {}/{}", repository.getProject().getKey(), repository.getSlug());
+ repositoryCloneSettingsDAO.save(repository.getId(), settings.getURL(), settings.getEnabled());
return ResponseFactory.ok().build();
}
diff --git a/src/main/resources/META-INF/spring/plugin-context.xml b/src/main/resources/META-INF/spring/plugin-context.xml
index b1f0519..0caa4e8 100644
--- a/src/main/resources/META-INF/spring/plugin-context.xml
+++ b/src/main/resources/META-INF/spring/plugin-context.xml
@@ -1,10 +1,10 @@
-
-
\ No newline at end of file
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:atlassian-scanner="http://www.atlassian.com/schema/atlassian-scanner"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
+ http://www.atlassian.com/schema/atlassian-scanner
+ http://www.atlassian.com/schema/atlassian-scanner/atlassian-scanner.xsd">
+
+
diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml
index 3182ad5..ee4d916 100644
--- a/src/main/resources/atlassian-plugin.xml
+++ b/src/main/resources/atlassian-plugin.xml
@@ -1,51 +1,56 @@
-
- ${project.description}
- ${project.version}
-
- true
- images/pluginIcon.png
- images/pluginLogo.png
-
+
+ ${project.description}
+ ${project.version}
+
+ true
+ images/pluginIcon.png
+ images/pluginLogo.png
+
-
-
+
+
-
-
- Log clone events to a remote service
- ${navBuilder.pluginServlets().path('log-on-clone', $project.key, $repository.slug).buildRelNoContext()}
-
+
+
+ Log clone events to a remote service
+ ${navBuilder.pluginServlets().path('log-on-clone', $project.key, $repository.slug).buildRelNoContext()}
+
-
- /log-on-clone/*
-
+
+ /log-on-clone/*
+
-
- REQUEST
- FORWARD
-
- extension.filter.excludes
- .*
-
- com.recuencojones.bitbucket.log.rest
-
+
+ REQUEST
+ FORWARD
+
+ extension.filter.excludes
+ .*
+
+ com.recuencojones.bitbucket.log.rest
+
-
-
- /**/*.soy
-
- com.atlassian.bitbucket.server.bitbucket-web:server-soy-templates
-
+
+ The module configuring the Active Objects service used by this plugin
+ com.recuencojones.bitbucket.log.dao.RepositoryCloneSettings
+
-
-
- /**/*.soy
-
- bitbucket.page.repository.settings.log-on-clone
- com.atlassian.bitbucket.server.bitbucket-web-api:navbuilder
- com.atlassian.bitbucket.server.bitbucket-web-api:server
- com.atlassian.bitbucket.server.bitbucket-web:global
- com.atlassian.auiplugin:aui-experimental-spinner
-
+
+
+ /**/*.soy
+
+ com.atlassian.bitbucket.server.bitbucket-web:server-soy-templates
+
+
+
+
+ /**/*.soy
+
+ bitbucket.page.repository.settings.log-on-clone
+ com.atlassian.bitbucket.server.bitbucket-web-api:navbuilder
+ com.atlassian.bitbucket.server.bitbucket-web-api:server
+ com.atlassian.bitbucket.server.bitbucket-web:global
+ com.atlassian.auiplugin:aui-experimental-spinner
+
diff --git a/src/main/resources/log-on-clone.properties b/src/main/resources/log-on-clone.properties
index fe8d29c..65c4f3c 100644
--- a/src/main/resources/log-on-clone.properties
+++ b/src/main/resources/log-on-clone.properties
@@ -1,5 +1,7 @@
bitbucket.web.repository.log-on-clone.title=Log on clone for {0} / {1}
bitbucket.web.repository.log-on-clone.heading=Log on clone
+bitbucket.web.repository.log-on-clone.setting.enabled.label=Enable
+bitbucket.web.repository.log-on-clone.setting.enabled.description=Call URL on clone event?
bitbucket.web.repository.log-on-clone.setting.url.label=URL
bitbucket.web.repository.log-on-clone.setting.url.placeholder=Logging service URL
bitbucket.web.repository.log-on-clone.setting.url.description=Target url should be a webhook listening on http/https. Event will be sent with POST method.
diff --git a/src/main/resources/static/clone-log-settings.css b/src/main/resources/static/clone-log-settings.css
index 745dc00..69a3c38 100644
--- a/src/main/resources/static/clone-log-settings.css
+++ b/src/main/resources/static/clone-log-settings.css
@@ -6,3 +6,10 @@
width: 20px;
top: 5px;
}
+
+.button-success {
+ display: inline-block;
+ top: -12px;
+ margin-left: 8px;
+ color: #35B37E;
+}
diff --git a/src/main/resources/static/clone-log-settings.js b/src/main/resources/static/clone-log-settings.js
index f575100..12fcbe2 100644
--- a/src/main/resources/static/clone-log-settings.js
+++ b/src/main/resources/static/clone-log-settings.js
@@ -1,11 +1,9 @@
define('plugin/log-on-clone/repository-settings', [
'jquery',
- 'aui',
'bitbucket/util/server',
'bitbucket/util/navbuilder',
- 'bitbucket/util/state',
'exports'
-], function($, AJS, server, nav, pageState, exports) {
+], function($, server, nav, exports) {
function resourceUrl(resourceName) {
return nav
.rest('log-on-clone')
@@ -15,14 +13,18 @@ define('plugin/log-on-clone/repository-settings', [
}
var formSelector = '#log-on-clone-settings-form';
- var inputSelector = 'input#url';
+ var urlInputSelector = 'input#url';
+ var enabledInputSelector = 'input#enabled';
+ var successTemplate = 'Done!';
var spinnerTemplate = '
';
function init() {
var $form = $(formSelector);
- var $input = $(inputSelector);
+ var $urlInput = $(urlInputSelector);
+ var $enabledInput = $(enabledInputSelector);
var $submit = $form.find('#log-on-clone-settings-form-submit');
var $spinner;
+ var $success;
function addSpinner() {
$spinner = $(spinnerTemplate).insertAfter($submit);
@@ -34,65 +36,81 @@ define('plugin/log-on-clone/repository-settings', [
$spinner = null;
}
- function setInputEnabled(enabled) {
- if (enabled) {
- $input.removeAttr('disabled').removeClass('disabled');
- } else {
- $input.attr('disabled', 'disabled').addClass('disabled');
- }
+ function flashSuccess() {
+ $success = $(successTemplate).insertAfter($submit);
+ setTimeout(function() {
+ $success && $success.remove();
+ $success = null;
+ }, 1000);
}
- function setSubmitEnabled(enabled) {
+ function setEnabled($el, enabled) {
if (enabled) {
- $submit.removeAttr('disabled').removeClass('disabled');
+ $el.removeAttr('disabled').removeClass('disabled');
} else {
- $submit.attr('disabled', 'disabled').addClass('disabled');
+ $el.attr('disabled', 'disabled').addClass('disabled');
}
}
- function setValue(value) {
- $input.val(value);
+ function setUrlValue(value) {
+ $urlInput.val(value);
+ }
+
+ function setEnabledValue(value) {
+ $enabledInput.prop('checked', value);
}
$form.submit(function(event) {
event.preventDefault();
addSpinner();
- setInputEnabled(false);
- setSubmitEnabled(false);
+ setEnabled($urlInput, false);
+ setEnabled($enabledInput, false);
+ setEnabled($submit, false);
server
.rest({
url: resourceUrl('settings'),
type: 'POST',
data: {
- url: $input.val()
+ url: $urlInput.val(),
+ enabled: $enabledInput.prop('checked')
}
})
.always(function() {
removeSpinner();
- setInputEnabled(true);
- setSubmitEnabled(true);
+ flashSuccess();
+ setEnabled($urlInput, true);
+ setEnabled($enabledInput, true);
+ setEnabled($submit, true);
});
});
- setInputEnabled(false);
- setSubmitEnabled(false);
+ addSpinner();
+ setEnabled($urlInput, false);
+ setEnabled($enabledInput, false);
+ setEnabled($submit, false);
server
.rest({
url: resourceUrl('settings'),
type: 'GET'
})
- .then(function() {
- setInputEnabled(true);
- setSubmitEnabled(true);
- setValue('https://en6qhxx7a3ksl.x.pipedream.net');
+ .then(function(response) {
+ if (response) {
+ setUrlValue(response.url);
+ setEnabledValue(response.enabled);
+ }
+
+ removeSpinner();
+ setEnabled($urlInput, true);
+ setEnabled($enabledInput, true);
+ setEnabled($submit, true);
});
}
exports.onReady = function() {
- $(document).ready(() => {
+ $(document).ready(function() {
init();
});
};
diff --git a/src/main/resources/static/clone-log-settings.soy b/src/main/resources/static/clone-log-settings.soy
index 1abbe08..551b9c4 100644
--- a/src/main/resources/static/clone-log-settings.soy
+++ b/src/main/resources/static/clone-log-settings.soy
@@ -50,6 +50,22 @@
* Actual settings form
*/
{template .settingsContent}
+ {call aui.form.fieldGroup}
+ {param content}
+ {call aui.form.label}
+ {param forField: 'enabled' /}
+ {param content: getText('bitbucket.web.repository.log-on-clone.setting.enabled.label') /}
+ {/call}
+ {call aui.form.input}
+ {param id: 'enabled' /}
+ {param name: 'enabled' /}
+ {param type: 'checkbox' /}
+ {/call}
+ {call aui.form.fieldDescription}
+ {param text: getText('bitbucket.web.repository.log-on-clone.setting.enabled.description') /}
+ {/call}
+ {/param}
+ {/call}
{call aui.form.fieldGroup}
{param content}
{call aui.form.label}
@@ -64,6 +80,7 @@
{param placeholderText: getText('bitbucket.web.repository.log-on-clone.setting.url.placeholder') /}
{param extraAttributes}
pattern="https?://.*"
+ required
{/param}
{/call}
{call aui.form.fieldDescription}
diff --git a/src/test/java/com/recuencojones/bitbucket/log/OnRepositoryCloneTest.java b/src/test/java/com/recuencojones/bitbucket/log/OnRepositoryCloneTest.java
new file mode 100644
index 0000000..ef5e271
--- /dev/null
+++ b/src/test/java/com/recuencojones/bitbucket/log/OnRepositoryCloneTest.java
@@ -0,0 +1,88 @@
+package com.recuencojones.bitbucket.log;
+
+import com.atlassian.bitbucket.event.repository.RepositoryCloneEvent;
+import com.atlassian.bitbucket.project.Project;
+import com.atlassian.bitbucket.repository.Repository;
+import com.atlassian.sal.api.net.Request;
+import com.atlassian.sal.api.net.RequestFactory;
+import com.atlassian.sal.api.net.ResponseException;
+import com.recuencojones.bitbucket.log.dao.RepositoryCloneSettings;
+import com.recuencojones.bitbucket.log.dao.RepositoryCloneSettingsDAO;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class OnRepositoryCloneTest {
+ final String url = "https://url";
+ final String projectKey = "proj_1";
+ final String repositorySlug = "repo_1";
+ final int repositoryID = 1337;
+
+ private Request mockRequest;
+ private RequestFactory mockRequestFactory;
+ private RepositoryCloneSettings mockRepositoryCloneSettings;
+ private RepositoryCloneSettingsDAO mockRepositoryCloneSettingsDAO;
+ private RepositoryCloneEvent mockEvent;
+ private Repository mockRepository;
+ private Project mockProject;
+
+ @Before
+ public void setup() {
+ mockRequest = mock(Request.class);
+ mockRequestFactory = mock(RequestFactory.class);
+ mockRepositoryCloneSettings = mock(RepositoryCloneSettings.class);
+ mockRepositoryCloneSettingsDAO = mock(RepositoryCloneSettingsDAO.class);
+ mockEvent = mock(RepositoryCloneEvent.class);
+ mockRepository = mock(Repository.class);
+ mockProject = mock(Project.class);
+
+ when(mockProject.getKey()).thenReturn(projectKey);
+ when(mockRepository.getId()).thenReturn(repositoryID);
+ when(mockRepository.getSlug()).thenReturn(repositorySlug);
+ when(mockRepository.getProject()).thenReturn(mockProject);
+ when(mockEvent.getRepository()).thenReturn(mockRepository);
+ when(mockRequestFactory.createRequest(Request.MethodType.POST, url)).thenReturn(mockRequest);
+ }
+
+ @Test
+ public void testCloneRepositoryWithSettingsShouldExecutePostRequestWhenEnabled() throws ResponseException {
+ final OnRepositoryClone component = new OnRepositoryClone(mockRequestFactory, mockRepositoryCloneSettingsDAO);
+
+ when(mockRepositoryCloneSettingsDAO.get(repositoryID)).thenReturn(mockRepositoryCloneSettings);
+ when(mockRepositoryCloneSettings.getURL()).thenReturn(url);
+ when(mockRepositoryCloneSettings.isEnabled()).thenReturn(true);
+
+ component.onCloneEvent(mockEvent);
+
+ verify(mockRequest).execute();
+ }
+
+ @Test
+ public void testCloneRepositoryWithSettingsShouldNotExecutePostRequestWhenNotEnabled() throws ResponseException {
+ final OnRepositoryClone component = new OnRepositoryClone(mockRequestFactory, mockRepositoryCloneSettingsDAO);
+
+ when(mockRepositoryCloneSettingsDAO.get(repositoryID)).thenReturn(mockRepositoryCloneSettings);
+ when(mockRepositoryCloneSettings.getURL()).thenReturn(url);
+ when(mockRepositoryCloneSettings.isEnabled()).thenReturn(false);
+
+ component.onCloneEvent(mockEvent);
+
+ verify(mockRequest, never()).execute();
+ }
+
+ @Test
+ public void testCloneRepositoryWithoutSettingsShouldNotExecutePostRequest() throws ResponseException {
+ final OnRepositoryClone component = new OnRepositoryClone(mockRequestFactory, mockRepositoryCloneSettingsDAO);
+
+ when(mockRepositoryCloneSettingsDAO.get(repositoryID)).thenReturn(null);
+
+ component.onCloneEvent(mockEvent);
+
+ verify(mockRequest, never()).execute();
+ }
+}
diff --git a/src/test/java/com/recuencojones/bitbucket/log/OnRepositoryDeletedTest.java b/src/test/java/com/recuencojones/bitbucket/log/OnRepositoryDeletedTest.java
new file mode 100644
index 0000000..3b90213
--- /dev/null
+++ b/src/test/java/com/recuencojones/bitbucket/log/OnRepositoryDeletedTest.java
@@ -0,0 +1,64 @@
+package com.recuencojones.bitbucket.log;
+
+import com.atlassian.bitbucket.event.repository.RepositoryDeletedEvent;
+import com.atlassian.bitbucket.project.Project;
+import com.atlassian.bitbucket.repository.Repository;
+import com.recuencojones.bitbucket.log.dao.RepositoryCloneSettings;
+import com.recuencojones.bitbucket.log.dao.RepositoryCloneSettingsDAO;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class OnRepositoryDeletedTest {
+ final String projectKey = "proj_1";
+ final String repositorySlug = "repo_1";
+ final int repositoryID = 1337;
+
+ private RepositoryCloneSettings mockRepositoryCloneSettings;
+ private RepositoryCloneSettingsDAO mockRepositoryCloneSettingsDAO;
+ private RepositoryDeletedEvent mockEvent;
+ private Repository mockRepository;
+ private Project mockProject;
+
+ @Before
+ public void setup() {
+ mockRepositoryCloneSettings = mock(RepositoryCloneSettings.class);
+ mockRepositoryCloneSettingsDAO = mock(RepositoryCloneSettingsDAO.class);
+ mockEvent = mock(RepositoryDeletedEvent.class);
+ mockRepository = mock(Repository.class);
+ mockProject = mock(Project.class);
+
+ when(mockProject.getKey()).thenReturn(projectKey);
+ when(mockRepository.getId()).thenReturn(repositoryID);
+ when(mockRepository.getSlug()).thenReturn(repositorySlug);
+ when(mockRepository.getProject()).thenReturn(mockProject);
+ when(mockEvent.getRepository()).thenReturn(mockRepository);
+ }
+
+ @Test
+ public void testDeleteRepositoryWithSettingsShouldRemoveSettings() {
+ final OnRepositoryDeleted component = new OnRepositoryDeleted(mockRepositoryCloneSettingsDAO);
+
+ when(mockRepositoryCloneSettingsDAO.get(repositoryID)).thenReturn(mockRepositoryCloneSettings);
+
+ component.onRepositoryDeleted(mockEvent);
+
+ verify(mockRepositoryCloneSettingsDAO).remove(mockRepositoryCloneSettings);
+ }
+
+ @Test
+ public void testDeleteRepositoryWithoutSettingsShouldNotRemoveSettings() {
+ final OnRepositoryDeleted component = new OnRepositoryDeleted(mockRepositoryCloneSettingsDAO);
+
+ when(mockRepositoryCloneSettingsDAO.get(repositoryID)).thenReturn(null);
+
+ component.onRepositoryDeleted(mockEvent);
+
+ verify(mockRepositoryCloneSettingsDAO, never()).remove(mockRepositoryCloneSettings);
+ }
+}
diff --git a/src/test/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsResourceTest.java b/src/test/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsResourceTest.java
new file mode 100644
index 0000000..a7f6089
--- /dev/null
+++ b/src/test/java/com/recuencojones/bitbucket/log/rest/RepositoryCloneSettingsResourceTest.java
@@ -0,0 +1,97 @@
+package com.recuencojones.bitbucket.log.rest;
+
+import com.atlassian.bitbucket.AuthorisationException;
+import com.atlassian.bitbucket.permission.Permission;
+import com.atlassian.bitbucket.permission.PermissionValidationService;
+import com.atlassian.bitbucket.project.Project;
+import com.atlassian.bitbucket.repository.Repository;
+import com.recuencojones.bitbucket.log.dao.RepositoryCloneSettings;
+import com.recuencojones.bitbucket.log.dao.RepositoryCloneSettingsDAO;
+
+import javax.ws.rs.core.Response;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class RepositoryCloneSettingsResourceTest {
+ final boolean enabled = true;
+ final String url = "https://url";
+ final String projectKey = "proj_1";
+ final String repositorySlug = "repo_1";
+ final int repositoryID = 1337;
+
+ private PermissionValidationService mockPermissionValidationService;
+ private RepositoryCloneSettings mockRepositoryCloneSettings;
+ private RepositoryCloneSettingsDAO mockRepositoryCloneSettingsDAO;
+ private Repository mockRepository;
+ private Project mockProject;
+
+ @Before
+ public void setup() {
+ mockPermissionValidationService = mock(PermissionValidationService.class);
+ mockRepositoryCloneSettings = mock(RepositoryCloneSettings.class);
+ mockRepositoryCloneSettingsDAO = mock(RepositoryCloneSettingsDAO.class);
+ mockRepository = mock(Repository.class);
+ mockProject = mock(Project.class);
+
+ when(mockProject.getKey()).thenReturn(projectKey);
+ when(mockRepository.getId()).thenReturn(repositoryID);
+ when(mockRepository.getSlug()).thenReturn(repositorySlug);
+ when(mockRepository.getProject()).thenReturn(mockProject);
+ }
+
+ @Test
+ public void testGetSettingsForRepositoryWithConfigShouldReturnSettingsObject() {
+ RepositoryCloneSettingsResource component = new RepositoryCloneSettingsResource(mockPermissionValidationService, mockRepositoryCloneSettingsDAO);
+
+ when(mockRepositoryCloneSettingsDAO.get(repositoryID)).thenReturn(mockRepositoryCloneSettings);
+
+ Response res = component.getSettings(mockRepository);
+
+ assertThat(res.getEntity(), instanceOf(RepositoryCloneSettingsDTO.class));
+ }
+
+ @Test
+ public void testGetSettingsForRepositoryWithoutConfigShouldReturnNull() {
+ RepositoryCloneSettingsResource component = new RepositoryCloneSettingsResource(mockPermissionValidationService, mockRepositoryCloneSettingsDAO);
+
+ when(mockRepositoryCloneSettingsDAO.get(repositoryID)).thenReturn(null);
+
+ Response res = component.getSettings(mockRepository);
+
+ assertNull(res.getEntity());
+ }
+
+ @Test
+ public void testSaveSettingsForRepositoryAdminShouldSucceed() {
+ RepositoryCloneSettingsResource component = new RepositoryCloneSettingsResource(mockPermissionValidationService, mockRepositoryCloneSettingsDAO);
+ RepositoryCloneSettingsDTO settings = new RepositoryCloneSettingsDTO();
+
+ settings.setEnabled(enabled);
+ settings.setURL(url);
+
+ component.saveSettings(mockRepository, settings);
+
+ verify(mockRepositoryCloneSettingsDAO).save(repositoryID, url, enabled);
+ }
+
+ @Test(expected = AuthorisationException.class)
+ public void testSaveSettingsForNonRepositoryAdminShouldFail() {
+ RepositoryCloneSettingsResource component = new RepositoryCloneSettingsResource(mockPermissionValidationService, mockRepositoryCloneSettingsDAO);
+ RepositoryCloneSettingsDTO settings = new RepositoryCloneSettingsDTO();
+
+ doThrow(AuthorisationException.class)
+ .when(mockPermissionValidationService)
+ .validateForRepository(mockRepository, Permission.REPO_ADMIN);
+
+ component.saveSettings(mockRepository, settings);
+ }
+}