Bitbucket中用于同步git仓库的插件,支持http(s)和ssh协议。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

343 lines
14 KiB

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.scm.git.command.GitScmCommandBuilder;
import com.atlassian.bitbucket.setting.RepositorySettingsValidator;
import com.atlassian.bitbucket.setting.Settings;
import com.atlassian.bitbucket.setting.SettingsValidationErrors;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator {
protected static class MirrorSettings {
String mirrorRepoUrl;
String username;
String password;
String suffix;
String refspec;
}
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_REFSPEC = "refspec";
static final int MAX_ATTEMPTS = 5;
static final String DEFAULT_REFSPEC = "+refs/heads/*:refs/heads/*";
private final ScmService scmService;
private final I18nService i18nService;
private final ScheduledExecutorService executor;
private final PasswordEncryptor passwordEncryptor;
private final SettingsReflectionHelper settingsReflectionHelper;
private final RepositoryService repositoryService;
private static final Logger logger = LoggerFactory.getLogger(MirrorRepositoryHook.class);
public MirrorRepositoryHook(
ScmService scmService,
I18nService i18nService,
ScheduledExecutorService executor,
PasswordEncryptor passwordEncryptor,
SettingsReflectionHelper settingsReflectionHelper,
PluginSettingsFactory pluginSettingsFactory,
RepositoryService repositoryService
) {
logger.debug("MirrorRepositoryHook: init started");
// Set fields
this.scmService = scmService;
this.i18nService = i18nService;
this.executor = executor;
this.passwordEncryptor = passwordEncryptor;
this.settingsReflectionHelper = settingsReflectionHelper;
this.repositoryService = repositoryService;
// Init password encryptor
PluginSettings pluginSettings = pluginSettingsFactory.createSettingsForKey(PLUGIN_SETTINGS_KEY);
passwordEncryptor.init(pluginSettings);
logger.debug("MirrorRepositoryHook: init completed");
}
/**
* Calls the remote bitbucket instance(s) to push the latest changes
* <p>
* Callback method that is called just after a push is completed (or a pull request accepted).
* This hook executes <i>after</i> the processing of a push and will not block the user client.
* <p>
* Despite being asynchronous, the user who initiated this change is still available from
*
* @param context the context which the hook is being run with
* @param refChanges the refs that have just been updated
*/
@Override
public void postReceive(
@Nonnull RepositoryHookContext context,
@Nonnull Collection<RefChange> refChanges) {
logger.debug("MirrorRepositoryHook: postReceive started.");
List<MirrorSettings> mirrorSettings = getMirrorSettings(context.getSettings());
for (MirrorSettings settings : mirrorSettings) {
runMirrorCommand(settings, context.getRepository());
}
}
void runMirrorCommand(MirrorSettings settings, final Repository repository) {
if (repositoryService.isEmpty(repository)) {
return;
}
try {
final String password = passwordEncryptor.decrypt(settings.password);
final String authenticatedUrl = getAuthenticatedUrl(settings.mirrorRepoUrl, settings.username, password);
executor.submit(new Runnable() {
int attempts = 0;
@Override
public void run() {
try {
ScmCommandBuilder obj = scmService.createBuilder(repository);
if (!(obj instanceof GitScmCommandBuilder)) {
logger.warn("Repository " + repository.getName() + " is not a git repo, cannot mirror");
return;
}
GitScmCommandBuilder builder = (GitScmCommandBuilder) obj;
PasswordHandler passwordHandler = getPasswordHandler(builder, password);
// 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
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 refspec args
String refspecs = Strings.isNullOrEmpty(settings.refspec) ? DEFAULT_REFSPEC : settings.refspec;
for (String refspec : refspecs.split("\\s|\\n")) {
if (!Strings.isNullOrEmpty(refspec)) {
builder.argument(refspec);
}
}
builder.errorHandler(passwordHandler)
.exitHandler(passwordHandler);
String result = builder.build(passwordHandler).call();
logger.debug("MirrorRepositoryHook: postReceive completed with result '{}'.", result);
} catch (Exception e) {
if (++attempts >= MAX_ATTEMPTS) {
logger.error("Failed to mirror repository " + repository.getName() + " after " + attempts
+ " attempts.", e);
} else {
logger.warn("Failed to mirror repository " + repository.getName() + ", " +
"retrying in 1 minute (attempt {} of {}).", attempts, MAX_ATTEMPTS);
executor.schedule(this, 1, TimeUnit.MINUTES);
}
}
}
});
} catch (Exception e) {
logger.error("MirrorRepositoryHook: Error running mirror hook", e);
}
}
protected String getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) throws URISyntaxException {
// Only http(s) has username/password
if (!mirrorRepoUrl.toLowerCase().startsWith("http")) {
return mirrorRepoUrl;
}
URI uri = URI.create(mirrorRepoUrl);
String userInfo = username + ":" + password;
return new URI(uri.getScheme(), userInfo, uri.getHost(), uri.getPort(),
uri.getPath(), uri.getQuery(), uri.getFragment()).toString();
}
/**
* Validate the given {@code settings} before they are persisted.
*
* @param settings to be validated
* @param errors callback for reporting validation errors.
* @param repository the context {@code Repository} the settings will be associated with
*/
@Override
public void validate(
@Nonnull Settings settings,
@Nonnull SettingsValidationErrors errors,
@Nonnull Repository repository) {
try {
boolean ok = true;
logger.debug("MirrorRepositoryHook: validate started.");
List<MirrorSettings> mirrorSettings = getMirrorSettings(settings);
for (MirrorSettings ms : mirrorSettings) {
if (!validate(ms, settings, errors)) {
ok = false;
}
}
// If no errors, run the mirror command
if (ok) {
updateSettings(mirrorSettings, settings);
for (MirrorSettings ms : mirrorSettings) {
runMirrorCommand(ms, repository);
}
}
} catch (Exception e) {
logger.error("Error running MirrorRepositoryHook validate.", e);
errors.addFormError(e.getMessage());
}
}
protected List<MirrorSettings> getMirrorSettings(Settings settings) {
List<MirrorSettings> results = new ArrayList<>();
Map<String, Object> allSettings = settings.asMap();
int count = 0;
for (String key : allSettings.keySet()) {
if (key.startsWith(SETTING_MIRROR_REPO_URL)) {
String suffix = key.substring(SETTING_MIRROR_REPO_URL.length());
MirrorSettings ms = new MirrorSettings();
ms.mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL + suffix, "");
ms.username = settings.getString(SETTING_USERNAME + suffix, "");
ms.password = settings.getString(SETTING_PASSWORD + suffix, "");
ms.refspec = (settings.getString(SETTING_REFSPEC + suffix, ""));
ms.suffix = String.valueOf(count++);
results.add(ms);
}
}
return results;
}
protected boolean validate(MirrorSettings ms, Settings settings, SettingsValidationErrors errors) {
boolean result = true;
boolean isHttp = false;
if (ms.mirrorRepoUrl.isEmpty()) {
result = false;
errors.addFieldError(SETTING_MIRROR_REPO_URL + ms.suffix, "The mirror repo url is required.");
} else {
try {
URI uri = URI.create(ms.mirrorRepoUrl);
String scheme = uri.getScheme().toLowerCase();
if (scheme.startsWith("http")) {
isHttp = true;
if (ms.mirrorRepoUrl.contains("@")) {
result = false;
errors.addFieldError(SETTING_MIRROR_REPO_URL + ms.suffix,
"The username and password should not be included.");
}
}
} catch (Exception ex) {
// Not a valid url, assume it is something git can read
}
}
// HTTP must have username and password
if (isHttp) {
if (ms.username.isEmpty()) {
result = false;
errors.addFieldError(SETTING_USERNAME + ms.suffix, "The username is required when using http(s).");
}
if (ms.password.isEmpty()) {
result = false;
errors.addFieldError(SETTING_PASSWORD + ms.suffix, "The password is required when using http(s).");
}
} else {
// Only http should have username or password
ms.password = ms.username = "";
}
if (!ms.refspec.isEmpty()) {
if (!ms.refspec.contains(":")) {
result = false;
errors.addFieldError(SETTING_REFSPEC + ms.suffix, "A refspec should be in the form <src>:<dest>.");
}
}
return result;
}
protected void updateSettings(List<MirrorSettings> mirrorSettings, Settings settings) {
Map<String, Object> values = new HashMap<String, Object>();
// Store each mirror setting
for (MirrorSettings ms : mirrorSettings) {
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_REFSPEC + ms.suffix, ms.refspec);
}
// Unfortunately the settings are stored in an immutable map, so need to cheat with reflection
settingsReflectionHelper.set(values, settings);
}
protected PasswordHandler getPasswordHandler(GitScmCommandBuilder builder, String password) {
try {
Method method = builder.getClass().getDeclaredMethod("createExitHandler");
method.setAccessible(true);
CommandExitHandler exitHandler = (CommandExitHandler) method.invoke(builder);
return new PasswordHandler(password, exitHandler);
} catch (Throwable t) {
logger.warn("Unable to create exit handler", t);
}
return new PasswordHandler(password, new DefaultCommandExitHandler(i18nService));
}
}