Browse Source

MirrorRepositoryHook - the mirror command is attempted up to 5 times with 1 minute intervals if it fails. Saving hook settings will trigger the mirror command to run. (fixes #3 and fixes #4)

pull/5/head
Adrian Gonzalez 11 years ago
parent
commit
d0308a968c
  1. 98
      src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java
  2. 8
      src/main/resources/atlassian-plugin.xml
  3. 72
      src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java

98
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<RefChange> 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<Void>() {
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) {

8
src/main/resources/atlassian-plugin.xml

@ -8,15 +8,13 @@
</plugin-info>
<!-- Components that are injected -->
<component-import key="gitScm" interface="com.atlassian.stash.scm.git.GitScm" />
<component-import key="i18nService" interface="com.atlassian.stash.i18n.I18nService" />
<component-import key="gitScm" interface="com.atlassian.stash.scm.git.GitScm"/>
<component-import key="i18nService" interface="com.atlassian.stash.i18n.I18nService"/>
<component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties"/>
<!-- add our i18n resource -->
<resource type="i18n" name="i18n" location="stash-hook-mirror"/>
<!-- import from the product container -->
<component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties"/>
<repository-hook name="Mirror Repository Hook" i18n-name-key="mirror-repository-hook.name"
key="mirror-repository-hook" class="com.englishtown.stash.hook.MirrorRepositoryHook">
<description key="mirror-repository-hook.description">Mirror Repository Hook</description>

72
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<String> 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<Callable<Void>> 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<RefChange> refChanges = new ArrayList<RefChange>();
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<Void> 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<Void> 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")

Loading…
Cancel
Save