<commit_before>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 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;
    }

    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 int MAX_ATTEMPTS = 5;

    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
                        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
                                .errorHandler(passwordHandler)
                                .exitHandler(passwordHandler)<commit_msg>mirror notes<commit_after>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 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;
    }

    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 int MAX_ATTEMPTS = 5;

    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 = <PASSWORD>(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
                        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)
{'fraction_non_alphanumeric': 0.07082900311775521, 'fraction_numerical': 0.003197697657686466, 'mean_word_length': 2.6514886164623466, 'pattern_counts': {'":': 0, '<': 16, '<?xml version=': 0, '>': 16, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7151923', 'n_tokens_mistral': 3148, 'n_tokens_neox': 2977, 'n_words': 757}