Browse Source

Merge remote-tracking branch 'upstream/develop' into develop

pull/55/head
Hansjörg Oppermann 7 years ago
parent
commit
d516b42d17
  1. 2
      README.md
  2. 88
      src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java
  3. 3
      src/main/resources/i18n/stash-hook-mirror.properties
  4. 10
      src/main/resources/static/mirror-repository-hook.soy
  5. 88
      src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java

2
README.md

@ -1,6 +1,6 @@
[![Build Status](https://travis-ci.org/ef-labs/stash-hook-mirror.png)](https://travis-ci.org/ef-labs/stash-hook-mirror)
#Bitbucket Server Repository Hook for Mirroring
# Bitbucket Server Repository Hook for Mirroring
The following is a plugin for Atlassian Bitbucket Server to provide repository mirroring to a remote repository.

88
src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java

@ -3,13 +3,8 @@ package com.englishtown.bitbucket.hook;
import com.atlassian.bitbucket.hook.repository.AsyncPostReceiveRepositoryHook;
import com.atlassian.bitbucket.hook.repository.RepositoryHookContext;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.scm.CommandExitHandler;
import com.atlassian.bitbucket.scm.DefaultCommandExitHandler;
import com.atlassian.bitbucket.scm.ScmCommandBuilder;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.*;
import com.atlassian.bitbucket.scm.git.command.GitScmCommandBuilder;
import com.atlassian.bitbucket.setting.RepositorySettingsValidator;
import com.atlassian.bitbucket.setting.Settings;
@ -26,6 +21,8 @@ import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator {
@ -34,12 +31,14 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
String username;
String password;
String suffix;
String branchesIncludePattern;
}
public static final String PLUGIN_SETTINGS_KEY = "com.englishtown.stash.hook.mirror";
static final String SETTING_MIRROR_REPO_URL = "mirrorRepoUrl";
static final String SETTING_USERNAME = "username";
static final String SETTING_PASSWORD = "password";
static final String SETTING_BRANCHES_INCLUDE_PATTERN = "branchesIncludePattern";
static final int MAX_ATTEMPTS = 5;
private final ScmService scmService;
@ -98,12 +97,12 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
List<MirrorSettings> mirrorSettings = getMirrorSettings(context.getSettings());
for (MirrorSettings settings : mirrorSettings) {
runMirrorCommand(settings, context.getRepository());
runMirrorCommand(settings, context.getRepository(), refChanges);
}
}
void runMirrorCommand(MirrorSettings settings, final Repository repository) {
void runMirrorCommand(MirrorSettings settings, final Repository repository, Collection<RefChange> refChanges) {
if (repositoryService.isEmpty(repository)) {
return;
}
@ -129,18 +128,19 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
// 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
String result = builder
.command("push")
.argument("--prune") // this deletes locally deleted branches
.argument(authenticatedUrl)
.argument("--force") // Canonical repository should always take precedence over mirror
.argument("+refs/heads/*:refs/heads/*") // Only mirror heads
.argument("+refs/tags/*:refs/tags/*") // and tags
.argument("+refs/notes/*:refs/notes/*") // and notes
.errorHandler(passwordHandler)
.exitHandler(passwordHandler)
.build(passwordHandler)
.call();
builder.command("push")
.argument("--prune") // this deletes locally deleted branches
.argument("--atomic") // use an atomic transaction to have a consistent state
.argument(authenticatedUrl)
.argument("--force") // Canonical repository should always take precedence over mirror
.argument("+refs/tags/*:refs/tags/*") // and tags
.argument("+refs/notes/*:refs/notes/*"); // and notes
// add branch arguments
addBranchArguments(settings, refChanges, builder);
builder.errorHandler(passwordHandler)
.exitHandler(passwordHandler);
String result = builder.build(passwordHandler).call();
logger.debug("MirrorRepositoryHook: postReceive completed with result '{}'.", result);
@ -207,7 +207,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
if (ok) {
updateSettings(mirrorSettings, settings);
for (MirrorSettings ms : mirrorSettings) {
runMirrorCommand(ms, repository);
runMirrorCommand(ms, repository, Collections.EMPTY_LIST);
}
}
@ -232,6 +232,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
ms.mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL + suffix, "");
ms.username = settings.getString(SETTING_USERNAME + suffix, "");
ms.password = settings.getString(SETTING_PASSWORD + suffix, "");
ms.branchesIncludePattern = (settings.getString(SETTING_BRANCHES_INCLUDE_PATTERN + suffix, ""));
ms.suffix = String.valueOf(count++);
results.add(ms);
@ -284,6 +285,15 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
ms.password = ms.username = "";
}
if (!ms.branchesIncludePattern.isEmpty()) {
try {
Pattern.compile(ms.branchesIncludePattern);
} catch (PatternSyntaxException e) {
result = false;
errors.addFieldError(SETTING_BRANCHES_INCLUDE_PATTERN + ms.suffix, "This is not a valid regular expression.");
}
}
return result;
}
@ -296,6 +306,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
values.put(SETTING_MIRROR_REPO_URL + ms.suffix, ms.mirrorRepoUrl);
values.put(SETTING_USERNAME + ms.suffix, ms.username);
values.put(SETTING_PASSWORD + ms.suffix, (ms.password.isEmpty() ? ms.password : passwordEncryptor.encrypt(ms.password)));
values.put(SETTING_BRANCHES_INCLUDE_PATTERN + ms.suffix, ms.branchesIncludePattern);
}
// Unfortunately the settings are stored in an immutable map, so need to cheat with reflection
@ -319,4 +330,37 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
return new PasswordHandler(password, new DefaultCommandExitHandler(i18nService));
}
private void addBranchArguments(MirrorSettings settings, Collection<RefChange> refChanges, CommandBuilder builder) {
Map<String, String> branchModifyArguments = new HashMap<>();
Map<String, String> branchDeleteArguments = new HashMap<>();
// if an empty list of RefChanges was provided we assume this was caused by triggering after a config change.
// same if no branch pattern was specified we sync all branches
if(refChanges.isEmpty() || settings.branchesIncludePattern.isEmpty()) {
builder.argument("+refs/heads/*:refs/heads/*");
} else {
for (RefChange refChange : refChanges) {
MinimalRef ref = refChange.getRef();
String displayId = ref.getDisplayId();
// branch operations
if (ref.getType().equals(StandardRefType.BRANCH)) {
if (displayId.matches(settings.branchesIncludePattern)) {
if (refChange.getType().equals(RefChangeType.DELETE) && !branchDeleteArguments.containsKey(displayId)) {
branchDeleteArguments.put(displayId, "+:refs/heads/" + displayId);
} else if ((refChange.getType().equals(RefChangeType.ADD) || refChange.getType().equals(RefChangeType.UPDATE)) && !branchModifyArguments.containsKey(displayId)) {
branchModifyArguments.put(displayId, "+refs/heads/" + displayId + ":refs/heads/" + displayId);
}
}
}
}
for (String key : branchDeleteArguments.keySet()) {
builder.argument(branchDeleteArguments.get(key));
}
for (String key : branchModifyArguments.keySet()) {
builder.argument(branchModifyArguments.get(key));
}
}
}
}

3
src/main/resources/i18n/stash-hook-mirror.properties

@ -10,3 +10,6 @@ mirror-repository-hook.username.description=The username to use for pushing to t
mirror-repository-hook.password.label=Password
mirror-repository-hook.password.description=The password to use for pushing to the mirror over http(s)
mirror-repository-hook.branchesIncludePattern.label=Branches Include Pattern
mirror-repository-hook.branchesIncludePattern.description=Regex pattern for branches to include

10
src/main/resources/static/mirror-repository-hook.soy

@ -73,6 +73,16 @@
{param extraClasses: 'long' /}
{param errorTexts: $errors ? $errors['password' + $index] : null /}
{/call}
{call aui.form.textField}
{param id: 'branchesIncludePattern' + $index /}
{param value: $config['branchesIncludePattern' + $index] /}
{param labelContent}
{getText('mirror-repository-hook.branchesIncludePattern.label')}
{/param}
{param descriptionText: getText('mirror-repository-hook.branchesIncludePattern.description') /}
{param extraClasses: 'long' /}
{param errorTexts: $errors ? $errors['branchesIncludePattern' + $index] : null /}
{/call}
</fieldset>
{/template}

88
src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java

@ -2,8 +2,7 @@ package com.englishtown.bitbucket.hook;
import com.atlassian.bitbucket.hook.repository.RepositoryHookContext;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.CommandErrorHandler;
import com.atlassian.bitbucket.scm.CommandExitHandler;
import com.atlassian.bitbucket.scm.CommandOutputHandler;
@ -23,9 +22,7 @@ import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@ -63,6 +60,7 @@ public class MirrorRepositoryHookTest {
private final String username = "test-user";
private final String password = "test-password";
private final String repository = "https://test-user:test-password@bitbucket-mirror.englishtown.com/scm/test/test.git";
private final String branchesIncludePattern ="master";
@Captor
ArgumentCaptor<Runnable> argumentCaptor;
@ -96,6 +94,44 @@ public class MirrorRepositoryHookTest {
verifyExecutor();
}
@Test
public void testPostReceiveWithBranchesPatternUpdateOperation() throws Exception {
when(passwordEncryptor.decrypt(anyString())).thenReturn(password);
Repository repo = mock(Repository.class);
RefChange refChange = mock(RefChange.class);
MinimalRef ref = mock(MinimalRef.class);
List<RefChange> refChanges = new ArrayList<>();
refChanges.add(refChange);
when(refChange.getRef()).thenReturn(ref);
when(refChange.getType()).thenReturn(RefChangeType.UPDATE);
when(ref.getType()).thenReturn(StandardRefType.BRANCH);
when(ref.getDisplayId()).thenReturn("master");
hook.postReceive(buildContext(repo), refChanges);
verifyExecutorWithBranchesPatternUpdateOperation();
}
@Test
public void testPostReceiveWithBranchesPatternDeleteOperation() throws Exception {
when(passwordEncryptor.decrypt(anyString())).thenReturn(password);
Repository repo = mock(Repository.class);
RefChange refChange = mock(RefChange.class);
MinimalRef ref = mock(MinimalRef.class);
List<RefChange> refChanges = new ArrayList<>();
refChanges.add(refChange);
when(refChange.getRef()).thenReturn(ref);
when(refChange.getType()).thenReturn(RefChangeType.DELETE);
when(ref.getType()).thenReturn(StandardRefType.BRANCH);
when(ref.getDisplayId()).thenReturn("master");
hook.postReceive(buildContext(repo), refChanges);
verifyExecutorWithBranchesPatternDeleteOperation();
}
@Test
public void testEmptyRepositoriesNotMirrored() {
Repository repo = mock(Repository.class);
@ -116,7 +152,7 @@ public class MirrorRepositoryHookTest {
ms.mirrorRepoUrl = mirrorRepoUrlHttp;
ms.username = username;
ms.password = password;
hook.runMirrorCommand(ms, mock(Repository.class));
hook.runMirrorCommand(ms, mock(Repository.class), Collections.emptyList());
verify(executor).submit(argumentCaptor.capture());
Runnable runnable = argumentCaptor.getValue();
@ -154,6 +190,7 @@ public class MirrorRepositoryHookTest {
verify(builder, times(1)).command(eq("push"));
verify(builder, times(1)).argument(eq("--prune"));
verify(builder, times(1)).argument(eq("--atomic"));
verify(builder, times(1)).argument(eq(repository));
verify(builder, times(1)).argument(eq("+refs/heads/*:refs/heads/*"));
verify(builder, times(1)).argument(eq("+refs/tags/*:refs/tags/*"));
@ -161,6 +198,38 @@ public class MirrorRepositoryHookTest {
}
private void verifyExecutorWithBranchesPatternUpdateOperation() throws Exception {
verify(executor).submit(argumentCaptor.capture());
Runnable runnable = argumentCaptor.getValue();
runnable.run();
verify(builder, times(1)).command(eq("push"));
verify(builder, times(1)).argument(eq("--prune"));
verify(builder, times(1)).argument(eq("--atomic"));
verify(builder, times(1)).argument(eq(repository));
verify(builder, times(1)).argument(eq("+refs/heads/master:refs/heads/master"));
verify(builder, times(1)).argument(eq("+refs/tags/*:refs/tags/*"));
verify(cmd, times(1)).call();
}
private void verifyExecutorWithBranchesPatternDeleteOperation() throws Exception {
verify(executor).submit(argumentCaptor.capture());
Runnable runnable = argumentCaptor.getValue();
runnable.run();
verify(builder, times(1)).command(eq("push"));
verify(builder, times(1)).argument(eq("--prune"));
verify(builder, times(1)).argument(eq("--atomic"));
verify(builder, times(1)).argument(eq(repository));
verify(builder, times(1)).argument(eq("+:refs/heads/master"));
verify(builder, times(1)).argument(eq("+refs/tags/*:refs/tags/*"));
verify(cmd, times(1)).call();
}
@Test
public void testGetAuthenticatedUrl() throws Exception {
@ -201,6 +270,11 @@ public class MirrorRepositoryHookTest {
.thenReturn("")
.thenReturn(password);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_BRANCHES_INCLUDE_PATTERN + "0"), eq("")))
.thenReturn("??")
.thenReturn("master")
.thenReturn("");
Repository repo = mock(Repository.class);
SettingsValidationErrors errors;
@ -215,6 +289,7 @@ public class MirrorRepositoryHookTest {
verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), anyString());
verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_USERNAME + "0"), anyString());
verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_PASSWORD + "0"), anyString());
verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_BRANCHES_INCLUDE_PATTERN + "0"), anyString());
errors = mock(SettingsValidationErrors.class);
hook.validate(settings, errors, repo);
@ -272,6 +347,7 @@ public class MirrorRepositoryHookTest {
when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), eq(""))).thenReturn(mirrorRepoUrlHttp);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME), eq(""))).thenReturn(username);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD), eq(""))).thenReturn(password);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_BRANCHES_INCLUDE_PATTERN), eq(""))).thenReturn(branchesIncludePattern);
return settings;
}
}

Loading…
Cancel
Save