Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
language: java
jdk:
- oraclejdk8
- openjdk8
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ https://bitbucket.org/atlassian/aui/src/master/src/soy/form.soy

The plugin can be found on the Atlassian Marketplace here:
https://marketplace.atlassian.com/plugins/com.englishtown.stash-hook-mirror

Plugin properties in bitbucket.properties in file, located in the shared folder of server home directory:

`plugin.com.englishtown.stash-hook-mirror.push.attempts=5
plugin.com.englishtown.stash-hook-mirror.push.threads=3
plugin.com.englishtown.stash-hook-mirror.push.timeout=120`

54 changes: 50 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.englishtown</groupId>
Expand Down Expand Up @@ -67,13 +68,53 @@
<artifactId>bitbucket-git-common</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.atlassian.sal</groupId>
<artifactId>sal-api</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-web-common</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.soy</groupId>
<artifactId>soy-template-renderer-api</artifactId>
<version>4.1.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.utils</groupId>
<artifactId>atlassian-processutils</artifactId>
<version>1.8.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.plugins.rest</groupId>
<artifactId>com.atlassian.jersey-library</artifactId>
<version>6.0.2</version>
<scope>provided</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13-atlassian-2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.soy</groupId>
<artifactId>atlassian-soy-spring-mvc-support</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-test-util</artifactId>
Expand All @@ -89,7 +130,12 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>2.25.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.ScmCommandBuilder;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.scm.*;
import com.atlassian.bitbucket.scm.git.command.GitCommandExitHandler;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.utils.process.ProcessException;
import com.atlassian.utils.process.StringOutputHandler;
import com.atlassian.utils.process.Watchdog;
import com.google.common.base.Strings;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -49,6 +55,7 @@ public MirrorBucketProcessor(I18nService i18nService, PasswordEncryptor password
this.securityService = securityService;

timeout = Duration.ofSeconds(propertiesService.getPluginProperty(PROP_TIMEOUT, 120L));
log.debug(PROP_TIMEOUT+": "+timeout.getSeconds());
}

@Override
Expand All @@ -75,16 +82,21 @@ public void process(@Nonnull String key, @Nonnull List<MirrorRequest> requests)
return null;
}
runMirrorCommand(request.getSettings(), repository);

return null;
});
}

private void runMirrorCommand(MirrorSettings settings, Repository repository) {
public void runMirrorCommand(MirrorSettings settings, Repository repository) {
runMirrorCommand(settings, repository, null);
}
public void runMirrorCommand(MirrorSettings settings, Repository repository, StringOutputHandler outputHandler) {
log.debug("{}: Preparing to push changes to mirror", repository);

String password = passwordEncryptor.decrypt(settings.password);
String authenticatedUrl = getAuthenticatedUrl(settings.mirrorRepoUrl, settings.username, password);
String plainPassword = passwordEncryptor.decrypt(settings.password);
String plainPrivateToken = passwordEncryptor.decrypt(settings.privateToken);
PasswordHandler passwordHandler = new PasswordHandler(plainPassword,plainPrivateToken,
new GitCommandExitHandler(i18nService, repository));
String authenticatedUrl = getAuthenticatedUrl(settings.mirrorRepoUrl, settings.username, plainPassword);

// Call push command with the prune flag and refspecs for heads and tags
// Do not use the mirror flag as pull-request refs are included
Expand Down Expand Up @@ -116,16 +128,36 @@ private void runMirrorCommand(MirrorSettings settings, Repository repository) {
builder.argument("+refs/notes/*:refs/notes/*");
}

PasswordHandler passwordHandler = new PasswordHandler(settings.password,
new GitCommandExitHandler(i18nService, repository));

Command<String> command = builder.errorHandler(passwordHandler)
.exitHandler(passwordHandler)
.build(passwordHandler);
command.setTimeout(timeout);

Object result = command.call();
log.info("{}: Push completed with the following output:\n{}", repository, result);
Object result=null;
try {
result = command.call();
if(result != null) {
passwordHandler.cleanText(result.toString());
}
} catch (Exception e) {
if (outputHandler != null) {
try {
outputHandler.process(new ByteArrayInputStream(passwordHandler.getOutput().getBytes()));
} catch (ProcessException s) {
log.error("Failed to process response: " + s.getMessage());
}
}
log.info("{}: Push failed with the following output:\n{}", repository, passwordHandler.getOutput());
throw e;
}
if (outputHandler != null) {
try {
outputHandler.process(new ByteArrayInputStream(passwordHandler.getOutput().getBytes()));
} catch (ProcessException s) {
log.error("Failed to process response: " + s.getMessage());
}
}
log.debug("{}: Push completed with the following output:\n{}", repository, passwordHandler.getOutput());
}

String getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) {
Expand Down
120 changes: 120 additions & 0 deletions src/main/java/com/englishtown/bitbucket/hook/MirrorRemoteAdmin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.englishtown.bitbucket.hook;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.git.command.GitCommandExitHandler;
import com.atlassian.utils.process.ProcessException;
import com.atlassian.utils.process.StringOutputHandler;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.core.MediaType;
import java.io.ByteArrayInputStream;
import java.io.IOException;

public class MirrorRemoteAdmin {
private static final Logger log = LoggerFactory.getLogger(MirrorRemoteAdmin.class);
private final PasswordEncryptor passwordEncryptor;
private final I18nService i18nService;

Client client;

public MirrorRemoteAdmin(PasswordEncryptor passwordEncryptor, I18nService i18nService) {
this.passwordEncryptor = passwordEncryptor;
this.i18nService = i18nService;
ClientConfig config = new DefaultClientConfig();
this.client = Client.create(config);
}

private void addToStream(StringOutputHandler outputHandler, String text) {
try {
outputHandler.process(new ByteArrayInputStream(text.getBytes()));
} catch (ProcessException e) {
log.error("Failed to process response: " + e.getMessage());
}
}

public void delete(MirrorSettings settings, Repository repository, StringOutputHandler outputHandler) {
String plainPassword = passwordEncryptor.decrypt(settings.password);
String plainPrivateToken = passwordEncryptor.decrypt(settings.privateToken);
PasswordHandler passwordHandler = new PasswordHandler(plainPassword, plainPrivateToken,
new GitCommandExitHandler(i18nService, repository));
RuntimeException e = null;
try {
delete(settings, repository, passwordHandler);
} catch (RuntimeException deleteException) {
e = deleteException;
}
addToStream(outputHandler, passwordHandler.getOutput());
if (e != null) {
throw e;
}
}

private void delete(MirrorSettings settings, Repository repository, PasswordHandler passwordHandler) {
ObjectMapper mapper = new ObjectMapper();
String plainPrivateToken = passwordEncryptor.decrypt(settings.privateToken);

if (settings.restApiURL.isEmpty()) {
log.error("Remote REST Api URL not configured for " + repository.getName());
return;
}
WebResource webResource = client
.resource(settings.restApiURL + "/api/v4/projects")
.queryParam("search", repository.getName());

WebResource.Builder webResourceBuilder= webResource.getRequestBuilder().accept(MediaType.APPLICATION_JSON);
if (!settings.privateToken.isEmpty()) {
webResourceBuilder = webResourceBuilder.header("PRIVATE-TOKEN",plainPrivateToken);
}

ClientResponse response = webResourceBuilder.get(ClientResponse.class);
if (response.getStatus() != 200) {
addToStream(passwordHandler, response.toString());
throw new RuntimeException("Failed : HTTP error code : "
+ response.getStatus());
}
JsonNode output;
try {
output = mapper.readTree(response.getEntityInputStream());
} catch (IOException e) {
addToStream(passwordHandler, e.toString());
throw new RuntimeException("Failed : Invalid response data from " + settings.restApiURL + " : " + e.getMessage());
}
Integer repoId = null;
for (JsonNode project : output) {
if (!project.has("name_with_namespace")) {
continue;
}
if (project.get("name_with_namespace").asText().equals(repository.getProject().getName() + " / " + repository.getName())) {
repoId = project.get("id").asInt();
break;
}
}
if (repoId == null) {
addToStream(passwordHandler, response.toString());
throw new RuntimeException("Remote repository not found");
}

webResource = client
.resource(settings.restApiURL + "/api/v4/projects/" + repoId);
webResourceBuilder= webResource.getRequestBuilder().accept(MediaType.APPLICATION_JSON);
if (!settings.privateToken.isEmpty()) {
webResourceBuilder = webResourceBuilder.header("PRIVATE-TOKEN",plainPrivateToken);
}
response = webResourceBuilder.delete(ClientResponse.class);
if (response.getStatus() != 202) {
addToStream(passwordHandler, response.toString());
throw new RuntimeException("Failed : HTTP error code : "
+ response.getStatus());
}
addToStream(passwordHandler, response.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.englishtown.bitbucket.hook;

import com.atlassian.bitbucket.hook.repository.RepositoryHook;
import com.atlassian.bitbucket.hook.repository.RepositoryHookService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scope.Scopes;
import com.atlassian.plugin.PluginParseException;
import com.atlassian.plugin.web.Condition;

import java.util.Map;

public class MirrorRepositoryHasConfigurationCondition implements Condition {

public static final String REPOSITORY = "repository";
private final RepositoryHookService repositoryHookService;

public MirrorRepositoryHasConfigurationCondition(RepositoryHookService repositoryHookService) {
this.repositoryHookService = repositoryHookService;
}

@Override
public void init(Map<String, String> map) throws PluginParseException {
}

@Override
public boolean shouldDisplay(Map<String, Object> context) {
Repository repository = (Repository) context.get(REPOSITORY);
RepositoryHook repositoryHook = repositoryHookService.getByKey(Scopes.repository(repository), "com.englishtown.stash-hook-mirror:mirror-repository-hook");
if (repositoryHook == null) {
/* Hook not installed. How do I get here :)? */
return false;
}
return repositoryHook.isConfigured() && repositoryHook.isEnabled();
}
}
Loading