|
|
|
@ -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") |
|
|
|
|
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/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(); |
|
|
|
|
.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)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|