diff --git a/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java index 08fd120..6cf3944 100644 --- a/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java +++ b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java @@ -19,20 +19,29 @@ import javax.annotation.Nonnull; import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator { 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 GitScm gitScm; private final I18nService i18nService; + private final ScheduledExecutorService executor; private static final Logger logger = LoggerFactory.getLogger(MirrorRepositoryHook.class); - public MirrorRepositoryHook(GitScm gitScm, I18nService i18nService) { + public MirrorRepositoryHook( + GitScm gitScm, + I18nService i18nService, + ScheduledExecutorService executor) { this.gitScm = gitScm; this.i18nService = i18nService; + this.executor = executor; } /** @@ -51,36 +60,63 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep @Nonnull RepositoryHookContext context, @Nonnull Collection refChanges) { + logger.debug("MirrorRepositoryHook: postReceive started."); + + Settings settings = context.getSettings(); + String mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL); + String username = settings.getString(SETTING_USERNAME); + String password = settings.getString(SETTING_PASSWORD); + + runMirrorCommand(mirrorRepoUrl, username, password, context.getRepository()); + + } + + void runMirrorCommand(String mirrorRepoUrl, String username, final String password, final Repository repository) { + try { - logger.debug("MirrorRepositoryHook: postReceive started."); - - Settings settings = context.getSettings(); - String mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL); - String username = settings.getString(SETTING_USERNAME); - String password = settings.getString(SETTING_PASSWORD); - - URI authenticatedUrl = getAuthenticatedUrl(mirrorRepoUrl, username, password); - GitScmCommandBuilder builder = gitScm.getCommandBuilderFactory().builder(context.getRepository()); - CommandExitHandler exitHandler = new GitCommandExitHandler(i18nService, context.getRepository()); - PasswordHandler passwordHandler = new PasswordHandler(password, exitHandler); - - // Call push command with the mirror flag set - String result = builder - .command("push") - .argument("--mirror") - .argument(authenticatedUrl.toString()) - .errorHandler(passwordHandler) - .exitHandler(passwordHandler) - .build(passwordHandler) - .call(); - - builder.defaultExitHandler(); - logger.debug("MirrorRepositoryHook: postReceive completed with result '{}'.", result); + final URI authenticatedUrl = getAuthenticatedUrl(mirrorRepoUrl, username, password); + + executor.submit(new Callable() { + + int attempts = 0; + + @Override + public Void call() throws Exception { + try { + GitScmCommandBuilder builder = gitScm.getCommandBuilderFactory().builder(repository); + CommandExitHandler exitHandler = new GitCommandExitHandler(i18nService, repository); + PasswordHandler passwordHandler = new PasswordHandler(password, exitHandler); + + // Call push command with the mirror flag set + String result = builder + .command("push") + .argument("--mirror") + .argument(authenticatedUrl.toString()) + .errorHandler(passwordHandler) + .exitHandler(passwordHandler) + .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."); + executor.schedule(this, 1, TimeUnit.MINUTES); + } + } + + return null; + } + }); } catch (Exception e) { logger.error("MirrorRepositoryHook: Error running mirror hook", e); } - } URI getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) throws URISyntaxException { @@ -129,16 +165,22 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep } } - if (settings.getString(SETTING_USERNAME, "").isEmpty()) { + String username = settings.getString(SETTING_USERNAME, ""); + if (username.isEmpty()) { count++; errors.addFieldError(SETTING_USERNAME, "The username is required."); } - if (settings.getString(SETTING_PASSWORD, "").isEmpty()) { + String password = settings.getString(SETTING_PASSWORD, ""); + if (password.isEmpty()) { count++; errors.addFieldError(SETTING_PASSWORD, "The password is required."); } + // If no errors, run the mirror command + if (count == 0) { + runMirrorCommand(mirrorRepoUrl, username, password, repository); + } logger.debug("MirrorRepositoryHook: validate completed with {} error(s).", count); } catch (Exception e) { diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml index ac337f0..fd4defe 100644 --- a/src/main/resources/atlassian-plugin.xml +++ b/src/main/resources/atlassian-plugin.xml @@ -8,15 +8,13 @@ - - + + + - - - Mirror Repository Hook diff --git a/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java b/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java index 2561e5f..10d2f30 100644 --- a/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java +++ b/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java @@ -15,10 +15,18 @@ import com.atlassian.stash.setting.Settings; import com.atlassian.stash.setting.SettingsValidationErrors; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; @@ -26,22 +34,28 @@ import static org.mockito.Mockito.*; /** * Unit tests for {@link MirrorRepositoryHook} */ +@RunWith(MockitoJUnitRunner.class) public class MirrorRepositoryHookTest { private MirrorRepositoryHook hook; private GitScmCommandBuilder builder; + @Mock private GitCommand cmd; + @Mock + private ScheduledExecutorService executor; private final String mirrorRepoUrl = "https://stash-mirror.englishtown.com/scm/test/test.git"; private final String username = "test-user"; private final String password = "test-password"; private final String repository = "https://test-user:test-password@stash-mirror.englishtown.com/scm/test/test.git"; - @SuppressWarnings("unchecked") + @SuppressWarnings("UnusedDeclaration") + @Captor + ArgumentCaptor> argumentCaptor; + @Before public void setup() { - cmd = mock(GitCommand.class); builder = mock(GitScmCommandBuilder.class); when(builder.command(anyString())).thenReturn(builder); when(builder.argument(anyString())).thenReturn(builder); @@ -55,7 +69,7 @@ public class MirrorRepositoryHookTest { GitScm gitScm = mock(GitScm.class); when(gitScm.getCommandBuilderFactory()).thenReturn(builderFactory); - hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class)); + hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor); } @@ -67,12 +81,62 @@ public class MirrorRepositoryHookTest { when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME))).thenReturn(username); when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD))).thenReturn(password); + Repository repo = mock(Repository.class); + when(repo.getName()).thenReturn("test"); + RepositoryHookContext context = mock(RepositoryHookContext.class); when(context.getSettings()).thenReturn(settings); + when(context.getRepository()).thenReturn(repo); Collection refChanges = new ArrayList(); hook.postReceive(context, refChanges); + verifyExecutor(); + } + + @Test + public void testRunMirrorCommand_Retries() throws Exception { + + GitScm gitScm = mock(GitScm.class); + when(gitScm.getCommandBuilderFactory()).thenThrow(new RuntimeException("Intentional unit test exception")); + MirrorRepositoryHook hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor); + hook.runMirrorCommand(mirrorRepoUrl, username, password, mock(Repository.class)); + + verify(executor).submit(argumentCaptor.capture()); + Callable callable = argumentCaptor.getValue(); + callable.call(); + + verify(executor, times(1)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class)); + callable = argumentCaptor.getValue(); + callable.call(); + + verify(executor, times(2)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class)); + callable = argumentCaptor.getValue(); + callable.call(); + + verify(executor, times(3)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class)); + callable = argumentCaptor.getValue(); + callable.call(); + + verify(executor, times(4)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class)); + callable = argumentCaptor.getValue(); + callable.call(); + + verify(executor, times(5)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class)); + callable = argumentCaptor.getValue(); + callable.call(); + + // Make sure it is only called 5 times + verify(executor, times(5)).schedule(argumentCaptor.capture(), anyInt(), any(TimeUnit.class)); + + } + + private void verifyExecutor() throws Exception { + + verify(executor).submit(argumentCaptor.capture()); + Callable callable = argumentCaptor.getValue(); + callable.call(); + verify(builder, times(1)).command(eq("push")); verify(builder, times(1)).argument(eq("--mirror")); verify(builder, times(1)).argument(eq(repository)); @@ -96,7 +160,7 @@ public class MirrorRepositoryHookTest { Settings settings = mock(Settings.class); when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), eq(""))) - .thenThrow(new RuntimeException()) + .thenThrow(new RuntimeException("Intentional unit test exception")) .thenReturn("") .thenReturn("invalid uri") .thenReturn("http://should-not:have-user@stash-mirror.englishtown.com/scm/test/test.git")