Browse Source

Merge pull request #54 from christiangalsterer/i53

i53: Mirror only specific branches
pull/60/merge
Adrian 7 years ago committed by GitHub
parent
commit
db64eba6ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 80
      src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java
  2. 3
      src/main/resources/i18n/stash-hook-mirror.properties
  3. 10
      src/main/resources/static/mirror-repository-hook.soy
  4. 88
      src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java

80
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.AsyncPostReceiveRepositoryHook;
import com.atlassian.bitbucket.hook.repository.RepositoryHookContext; import com.atlassian.bitbucket.hook.repository.RepositoryHookContext;
import com.atlassian.bitbucket.i18n.I18nService; import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.RefChange; import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.repository.Repository; import com.atlassian.bitbucket.scm.*;
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.scm.git.command.GitScmCommandBuilder; import com.atlassian.bitbucket.scm.git.command.GitScmCommandBuilder;
import com.atlassian.bitbucket.setting.RepositorySettingsValidator; import com.atlassian.bitbucket.setting.RepositorySettingsValidator;
import com.atlassian.bitbucket.setting.Settings; import com.atlassian.bitbucket.setting.Settings;
@ -26,6 +21,8 @@ import java.net.URISyntaxException;
import java.util.*; import java.util.*;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator { public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator {
@ -34,12 +31,14 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
String username; String username;
String password; String password;
String suffix; String suffix;
String branchesIncludePattern;
} }
public static final String PLUGIN_SETTINGS_KEY = "com.englishtown.stash.hook.mirror"; public static final String PLUGIN_SETTINGS_KEY = "com.englishtown.stash.hook.mirror";
static final String SETTING_MIRROR_REPO_URL = "mirrorRepoUrl"; static final String SETTING_MIRROR_REPO_URL = "mirrorRepoUrl";
static final String SETTING_USERNAME = "username"; static final String SETTING_USERNAME = "username";
static final String SETTING_PASSWORD = "password"; static final String SETTING_PASSWORD = "password";
static final String SETTING_BRANCHES_INCLUDE_PATTERN = "branchesIncludePattern";
static final int MAX_ATTEMPTS = 5; static final int MAX_ATTEMPTS = 5;
private final ScmService scmService; private final ScmService scmService;
@ -98,12 +97,12 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
List<MirrorSettings> mirrorSettings = getMirrorSettings(context.getSettings()); List<MirrorSettings> mirrorSettings = getMirrorSettings(context.getSettings());
for (MirrorSettings settings : mirrorSettings) { 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)) { if (repositoryService.isEmpty(repository)) {
return; return;
} }
@ -129,18 +128,19 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
// Call push command with the prune flag and refspecs for heads and tags // 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 // Do not use the mirror flag as pull-request refs are included
String result = builder builder.command("push")
.command("push")
.argument("--prune") // this deletes locally deleted branches .argument("--prune") // this deletes locally deleted branches
.argument("--atomic") // use an atomic transaction to have a consistent state
.argument(authenticatedUrl) .argument(authenticatedUrl)
.argument("--force") // Canonical repository should always take precedence over mirror .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/tags/*:refs/tags/*") // and tags
.argument("+refs/notes/*:refs/notes/*") // and notes .argument("+refs/notes/*:refs/notes/*"); // and notes
.errorHandler(passwordHandler) // add branch arguments
.exitHandler(passwordHandler) addBranchArguments(settings, refChanges, builder);
.build(passwordHandler) builder.errorHandler(passwordHandler)
.call(); .exitHandler(passwordHandler);
String result = builder.build(passwordHandler).call();
logger.debug("MirrorRepositoryHook: postReceive completed with result '{}'.", result); logger.debug("MirrorRepositoryHook: postReceive completed with result '{}'.", result);
@ -207,7 +207,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
if (ok) { if (ok) {
updateSettings(mirrorSettings, settings); updateSettings(mirrorSettings, settings);
for (MirrorSettings ms : mirrorSettings) { 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.mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL + suffix, "");
ms.username = settings.getString(SETTING_USERNAME + suffix, ""); ms.username = settings.getString(SETTING_USERNAME + suffix, "");
ms.password = settings.getString(SETTING_PASSWORD + suffix, ""); ms.password = settings.getString(SETTING_PASSWORD + suffix, "");
ms.branchesIncludePattern = (settings.getString(SETTING_BRANCHES_INCLUDE_PATTERN + suffix, ""));
ms.suffix = String.valueOf(count++); ms.suffix = String.valueOf(count++);
results.add(ms); results.add(ms);
@ -284,6 +285,15 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep
ms.password = ms.username = ""; 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; 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_MIRROR_REPO_URL + ms.suffix, ms.mirrorRepoUrl);
values.put(SETTING_USERNAME + ms.suffix, ms.username); 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_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 // 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)); 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.label=Password
mirror-repository-hook.password.description=The password to use for pushing to the mirror over http(s) 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

@ -71,6 +71,16 @@
{param extraClasses: 'long' /} {param extraClasses: 'long' /}
{param errorTexts: $errors ? $errors['password' + $index] : null /} {param errorTexts: $errors ? $errors['password' + $index] : null /}
{/call} {/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> </fieldset>
{/template} {/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.hook.repository.RepositoryHookContext;
import com.atlassian.bitbucket.i18n.I18nService; import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.repository.Repository; import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.scm.CommandErrorHandler; import com.atlassian.bitbucket.scm.CommandErrorHandler;
import com.atlassian.bitbucket.scm.CommandExitHandler; import com.atlassian.bitbucket.scm.CommandExitHandler;
import com.atlassian.bitbucket.scm.CommandOutputHandler; import com.atlassian.bitbucket.scm.CommandOutputHandler;
@ -23,9 +22,7 @@ import org.mockito.Matchers;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner; import org.mockito.runners.MockitoJUnitRunner;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -63,6 +60,7 @@ public class MirrorRepositoryHookTest {
private final String username = "test-user"; private final String username = "test-user";
private final String password = "test-password"; 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 repository = "https://test-user:test-password@bitbucket-mirror.englishtown.com/scm/test/test.git";
private final String branchesIncludePattern ="master";
@Captor @Captor
ArgumentCaptor<Runnable> argumentCaptor; ArgumentCaptor<Runnable> argumentCaptor;
@ -96,6 +94,44 @@ public class MirrorRepositoryHookTest {
verifyExecutor(); 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 @Test
public void testEmptyRepositoriesNotMirrored() { public void testEmptyRepositoriesNotMirrored() {
Repository repo = mock(Repository.class); Repository repo = mock(Repository.class);
@ -116,7 +152,7 @@ public class MirrorRepositoryHookTest {
ms.mirrorRepoUrl = mirrorRepoUrlHttp; ms.mirrorRepoUrl = mirrorRepoUrlHttp;
ms.username = username; ms.username = username;
ms.password = password; ms.password = password;
hook.runMirrorCommand(ms, mock(Repository.class)); hook.runMirrorCommand(ms, mock(Repository.class), Collections.emptyList());
verify(executor).submit(argumentCaptor.capture()); verify(executor).submit(argumentCaptor.capture());
Runnable runnable = argumentCaptor.getValue(); Runnable runnable = argumentCaptor.getValue();
@ -154,6 +190,7 @@ public class MirrorRepositoryHookTest {
verify(builder, times(1)).command(eq("push")); verify(builder, times(1)).command(eq("push"));
verify(builder, times(1)).argument(eq("--prune")); 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(repository));
verify(builder, times(1)).argument(eq("+refs/heads/*:refs/heads/*")); verify(builder, times(1)).argument(eq("+refs/heads/*:refs/heads/*"));
verify(builder, times(1)).argument(eq("+refs/tags/*:refs/tags/*")); 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 @Test
public void testGetAuthenticatedUrl() throws Exception { public void testGetAuthenticatedUrl() throws Exception {
@ -201,6 +270,11 @@ public class MirrorRepositoryHookTest {
.thenReturn("") .thenReturn("")
.thenReturn(password); .thenReturn(password);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_BRANCHES_INCLUDE_PATTERN + "0"), eq("")))
.thenReturn("??")
.thenReturn("master")
.thenReturn("");
Repository repo = mock(Repository.class); Repository repo = mock(Repository.class);
SettingsValidationErrors errors; SettingsValidationErrors errors;
@ -215,6 +289,7 @@ public class MirrorRepositoryHookTest {
verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), anyString()); 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_USERNAME + "0"), anyString());
verify(errors, never()).addFieldError(eq(MirrorRepositoryHook.SETTING_PASSWORD + "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); errors = mock(SettingsValidationErrors.class);
hook.validate(settings, errors, repo); 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_MIRROR_REPO_URL), eq(""))).thenReturn(mirrorRepoUrlHttp);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME), eq(""))).thenReturn(username); 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_PASSWORD), eq(""))).thenReturn(password);
when(settings.getString(eq(MirrorRepositoryHook.SETTING_BRANCHES_INCLUDE_PATTERN), eq(""))).thenReturn(branchesIncludePattern);
return settings; return settings;
} }
} }

Loading…
Cancel
Save