From 1f0025c66edcf1b85d230ffaa462d306ef2dac78 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez Date: Mon, 29 Apr 2013 21:32:02 -0400 Subject: [PATCH] Initial commit --- .gitignore | 3 + LICENSE | 20 ++ README.md | 14 ++ pom.xml | 185 ++++++++++++++++++ .../stash/hook/MirrorRepositoryHook.java | 151 ++++++++++++++ .../stash/hook/PasswordHandler.java | 50 +++++ src/main/resources/atlassian-plugin.xml | 29 +++ src/main/resources/icons/mirror-icon.png | Bin 0 -> 27696 bytes .../resources/stash-hook-mirror.properties | 4 + .../static/mirror-repository-hook.soy | 44 +++++ .../stash/hook/MirrorRepositoryHookTest.java | 146 ++++++++++++++ .../stash/hook/PasswordHandlerTest.java | 59 ++++++ 12 files changed, 705 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java create mode 100644 src/main/java/com/englishtown/stash/hook/PasswordHandler.java create mode 100644 src/main/resources/atlassian-plugin.xml create mode 100644 src/main/resources/icons/mirror-icon.png create mode 100644 src/main/resources/stash-hook-mirror.properties create mode 100644 src/main/resources/static/mirror-repository-hook.soy create mode 100644 src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java create mode 100644 src/test/java/com/englishtown/stash/hook/PasswordHandlerTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c2bc1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +\.idea/ +*.iml +target/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9b1d4a1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright © 2013 Englishtown + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9899ee2 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +#Stash Repository Hook for Mirroring + +The following is a plugin for Atlassian Stash to provide repository mirroring to a remote repository. + + +* atlas-run -- installs this plugin into the product and starts it on localhost +* atlas-debug -- same as atlas-run, but allows a debugger to attach at port 5005 +* atlas-cli -- after atlas-run or atlas-debug, opens a Maven command line window: + - 'pi' reinstalls the plugin into the running product instance +* atlas-help -- prints description for all commands in the SDK + +Full documentation is always available at: + +https://developer.atlassian.com/display/DOCS/Introduction+to+the+Atlassian+Plugin+SDK diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..70ea9d2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,185 @@ + + + + 4.0.0 + + com.englishtown + stash-hook-mirror + 1.0.0-SNAPSHOT + + + Englishtown + http://www.englishtown.com/ + + + Stash Mirror Plugin + A stash repository hook for mirroring to a remote repository. + atlassian-plugin + + + 2.3.1 + 2.3.1 + 4.1.7 + 1.1.1 + + 4.10 + 2.6 + 3.1 + 1.8.5 + 2.2.2-atlassian-1 + 1.1.1 + 1.7.5 + + + + + + com.atlassian.stash + stash-parent + ${stash.version} + pom + import + + + + + + + com.atlassian.stash + stash-scm-git-api + provided + + + org.slf4j + slf4j-api + ${slf4j.version} + provided + + + com.atlassian.stash + stash-scm-git + + + com.atlassian.stash + stash-api + provided + + + com.atlassian.stash + stash-spi + provided + + + com.atlassian.stash + stash-page-objects + provided + + + javax.servlet + servlet-api + provided + + + junit + junit + ${junit.version} + test + + + commons-lang + commons-lang + ${common-lang.version} + + + + com.atlassian.plugins + atlassian-plugins-osgi-testrunner + ${plugin.testrunner.version} + test + + + javax.ws.rs + jsr311-api + ${jsr311.version} + provided + + + com.google.code.gson + gson + ${gson.version} + + + org.mockito + mockito-all + ${mockito.version} + test + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + + + + + com.atlassian.maven.plugins + maven-stash-plugin + ${amps.version} + true + + + + stash + stash + ${stash.version} + ${stash.data.version} + + + + + + maven-compiler-plugin + ${plugin.compiler.version} + + 1.6 + 1.6 + + + + + + + + atlassian-public + https://m2proxy.atlassian.com/repository/public + + true + daily + warn + + + true + warn + + + + + + + atlassian-public + https://m2proxy.atlassian.com/repository/public + + true + warn + + + warn + + + + + diff --git a/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java new file mode 100644 index 0000000..08fd120 --- /dev/null +++ b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java @@ -0,0 +1,151 @@ +package com.englishtown.stash.hook; + +import com.atlassian.stash.hook.repository.AsyncPostReceiveRepositoryHook; +import com.atlassian.stash.hook.repository.RepositoryHookContext; +import com.atlassian.stash.i18n.I18nService; +import com.atlassian.stash.internal.scm.git.GitCommandExitHandler; +import com.atlassian.stash.repository.RefChange; +import com.atlassian.stash.repository.Repository; +import com.atlassian.stash.scm.CommandExitHandler; +import com.atlassian.stash.scm.git.GitScm; +import com.atlassian.stash.scm.git.GitScmCommandBuilder; +import com.atlassian.stash.setting.RepositorySettingsValidator; +import com.atlassian.stash.setting.Settings; +import com.atlassian.stash.setting.SettingsValidationErrors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; + +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"; + + private final GitScm gitScm; + private final I18nService i18nService; + private static final Logger logger = LoggerFactory.getLogger(MirrorRepositoryHook.class); + + public MirrorRepositoryHook(GitScm gitScm, I18nService i18nService) { + this.gitScm = gitScm; + this.i18nService = i18nService; + } + + /** + * Calls the remote stash instance(s) to push the latest changes + *

+ * Callback method that is called just after a push is completed (or a pull request accepted). + * This hook executes after the processing of a push and will not block the user client. + *

+ * 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 refChanges) { + + 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); + + } catch (Exception e) { + logger.error("MirrorRepositoryHook: Error running mirror hook", e); + } + + } + + URI getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) throws URISyntaxException { + + 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()); + + } + + /** + * 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 { + int count = 0; + logger.debug("MirrorRepositoryHook: validate started."); + + String mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL, ""); + if (mirrorRepoUrl.isEmpty()) { + count++; + errors.addFieldError(SETTING_MIRROR_REPO_URL, "The mirror repo url is required."); + } else { + URI uri; + try { + uri = URI.create(mirrorRepoUrl); + if (!uri.getScheme().toLowerCase().startsWith("http") || mirrorRepoUrl.contains("@")) { + count++; + errors.addFieldError(SETTING_MIRROR_REPO_URL, "The mirror repo url must be a valid http(s) " + + "URI and the user should be specified separately."); + } + } catch (Exception ex) { + count++; + errors.addFieldError(SETTING_MIRROR_REPO_URL, "The mirror repo url must be a valid http(s) URI."); + } + } + + if (settings.getString(SETTING_USERNAME, "").isEmpty()) { + count++; + errors.addFieldError(SETTING_USERNAME, "The username is required."); + } + + if (settings.getString(SETTING_PASSWORD, "").isEmpty()) { + count++; + errors.addFieldError(SETTING_PASSWORD, "The password is required."); + } + + logger.debug("MirrorRepositoryHook: validate completed with {} error(s).", count); + + } catch (Exception e) { + logger.error("Error running MirrorRepositoryHook validate.", e); + errors.addFormError(e.getMessage()); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/com/englishtown/stash/hook/PasswordHandler.java b/src/main/java/com/englishtown/stash/hook/PasswordHandler.java new file mode 100644 index 0000000..b935c97 --- /dev/null +++ b/src/main/java/com/englishtown/stash/hook/PasswordHandler.java @@ -0,0 +1,50 @@ +package com.englishtown.stash.hook; + +import com.atlassian.stash.scm.CommandErrorHandler; +import com.atlassian.stash.scm.CommandExitHandler; +import com.atlassian.stash.scm.CommandOutputHandler; +import com.atlassian.utils.process.StringOutputHandler; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Handles removing passwords from output text + */ +class PasswordHandler extends StringOutputHandler + implements CommandOutputHandler, CommandErrorHandler, CommandExitHandler { + + private final String target; + private final CommandExitHandler exitHandler; + + private static final String PASSWORD_REPLACEMENT = ":*****@"; + + public PasswordHandler(String password, CommandExitHandler exitHandler) { + this.exitHandler = exitHandler; + this.target = ":" + password + "@"; + } + + public String cleanText(String text) { + if (text == null || text.isEmpty()) { + return text; + } + return text.replace(target, PASSWORD_REPLACEMENT); + } + + @Override + public String getOutput() { + return cleanText(super.getOutput()); + } + + @Override + public void onCancel(@Nonnull String command, int exitCode, @Nullable String stdErr, @Nullable Throwable thrown) { + exitHandler.onCancel(cleanText(command), exitCode, cleanText(stdErr), thrown); + } + + @Override + public void onExit(@Nonnull String command, int exitCode, @Nullable String stdErr, @Nullable Throwable thrown) { + exitHandler.onExit(cleanText(command), exitCode, cleanText(stdErr), thrown); + } + +} + diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml new file mode 100644 index 0000000..ac337f0 --- /dev/null +++ b/src/main/resources/atlassian-plugin.xml @@ -0,0 +1,29 @@ + + + + + ${project.description} + ${project.version} + + + + + + + + + + + + + + + Mirror Repository Hook + /icons/mirror-icon.png + + com.englishtown.stash.hook.mirrorrepositoryhook.view + + + + diff --git a/src/main/resources/icons/mirror-icon.png b/src/main/resources/icons/mirror-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..84ff9ed9f9b322a5ebc02fc9fb035a74f3a2a9c6 GIT binary patch literal 27696 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 cmd; + + 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") + @Before + public void setup() { + + cmd = mock(GitCommand.class); + builder = mock(GitScmCommandBuilder.class); + when(builder.command(anyString())).thenReturn(builder); + when(builder.argument(anyString())).thenReturn(builder); + when(builder.errorHandler(any(CommandErrorHandler.class))).thenReturn(builder); + when(builder.exitHandler(any(CommandExitHandler.class))).thenReturn(builder); + when(builder.build(any(CommandOutputHandler.class))).thenReturn(cmd); + + GitCommandBuilderFactory builderFactory = mock(GitCommandBuilderFactory.class); + when(builderFactory.builder(any(Repository.class))).thenReturn(builder); + + GitScm gitScm = mock(GitScm.class); + when(gitScm.getCommandBuilderFactory()).thenReturn(builderFactory); + + hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class)); + + } + + @Test + public void testPostReceive() throws Exception { + + Settings settings = mock(Settings.class); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL))).thenReturn(mirrorRepoUrl); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME))).thenReturn(username); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD))).thenReturn(password); + + RepositoryHookContext context = mock(RepositoryHookContext.class); + when(context.getSettings()).thenReturn(settings); + + Collection refChanges = new ArrayList(); + + hook.postReceive(context, refChanges); + verify(builder, times(1)).command(eq("push")); + verify(builder, times(1)).argument(eq("--mirror")); + verify(builder, times(1)).argument(eq(repository)); + verify(cmd, times(1)).call(); + + } + + @Test + public void testGetAuthenticatedUrl() throws Exception { + + URI result; + + result = hook.getAuthenticatedUrl(mirrorRepoUrl, username, password); + assertEquals(repository, result.toString()); + + } + + @Test + public void testValidate() throws Exception { + + Settings settings = mock(Settings.class); + + when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), eq(""))) + .thenThrow(new RuntimeException()) + .thenReturn("") + .thenReturn("invalid uri") + .thenReturn("http://should-not:have-user@stash-mirror.englishtown.com/scm/test/test.git") + .thenReturn(mirrorRepoUrl); + + when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME), eq(""))) + .thenReturn("") + .thenReturn(username); + + when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD), eq(""))) + .thenReturn("") + .thenReturn(password); + + Repository repo = mock(Repository.class); + SettingsValidationErrors errors; + + errors = mock(SettingsValidationErrors.class); + hook.validate(settings, errors, repo); + verify(errors, times(1)).addFormError(anyString()); + + errors = mock(SettingsValidationErrors.class); + hook.validate(settings, errors, repo); + verify(errors, never()).addFormError(anyString()); + verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), anyString()); + verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_USERNAME), anyString()); + verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_PASSWORD), anyString()); + + errors = mock(SettingsValidationErrors.class); + hook.validate(settings, errors, repo); + verify(errors, never()).addFormError(anyString()); + verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), anyString()); + verify(errors).addFieldError(anyString(), anyString()); + + errors = mock(SettingsValidationErrors.class); + hook.validate(settings, errors, repo); + verify(errors, never()).addFormError(anyString()); + verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), anyString()); + verify(errors).addFieldError(anyString(), anyString()); + + errors = mock(SettingsValidationErrors.class); + hook.validate(settings, errors, repo); + verify(errors, never()).addFormError(anyString()); + verify(errors, never()).addFieldError(anyString(), anyString()); + + } + +} diff --git a/src/test/java/com/englishtown/stash/hook/PasswordHandlerTest.java b/src/test/java/com/englishtown/stash/hook/PasswordHandlerTest.java new file mode 100644 index 0000000..bb8931c --- /dev/null +++ b/src/test/java/com/englishtown/stash/hook/PasswordHandlerTest.java @@ -0,0 +1,59 @@ +package com.englishtown.stash.hook; + +import com.atlassian.stash.scm.CommandExitHandler; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link PasswordHandler} + */ +public class PasswordHandlerTest { + + @SuppressWarnings("FieldCanBeLocal") + private final String password = "pwd@123"; + private final String secretText = "https://test.user:pwd@123@test.englishtown.com/scm/test/test.git"; + private final String cleanedText = "https://test.user:*****@test.englishtown.com/scm/test/test.git"; + private CommandExitHandler exitHandler; + private PasswordHandler handler; + + @Before + public void setup() throws Exception { + exitHandler = mock(CommandExitHandler.class); + handler = new PasswordHandler(password, exitHandler); + } + + @Test + public void testCleanText() throws Exception { + + String result = handler.cleanText(secretText); + assertEquals(cleanedText, result); + + } + + @Test + public void testGetOutput() throws Exception { + + String result = handler.getOutput(); + assertEquals("", result); + + } + + @Test + public void testOnCancel() throws Exception { + + handler.onCancel(secretText, 0, secretText, null); + verify(exitHandler).onCancel(eq(cleanedText), eq(0), eq(cleanedText), any(Throwable.class)); + + } + + @Test + public void testOnExit() throws Exception { + + handler.onExit(secretText, 0, secretText, null); + verify(exitHandler).onExit(eq(cleanedText), eq(0), eq(cleanedText), any(Throwable.class)); + + } +}