diff --git a/gradle/changelog/url_import_post_receive_hook.yaml b/gradle/changelog/url_import_post_receive_hook.yaml new file mode 100644 index 0000000000..0a2213b9b0 --- /dev/null +++ b/gradle/changelog/url_import_post_receive_hook.yaml @@ -0,0 +1,2 @@ +- type: changed + description: Fire post receive repository hook event after the repository import has been finished. ([#1544](https://github.com/scm-manager/scm-manager/pull/1544)) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitConvertingChangesetIterable.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitConvertingChangesetIterable.java new file mode 100644 index 0000000000..d551b07e3d --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitConvertingChangesetIterable.java @@ -0,0 +1,67 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import org.eclipse.jgit.revwalk.RevCommit; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.GitChangesetConverter; + +import java.util.Iterator; + +class GitConvertingChangesetIterable implements Iterable { + + private final Iterable commitIterable; + private final GitChangesetConverter converter; + + GitConvertingChangesetIterable(Iterable commitIterable, + GitChangesetConverter converter) { + this.commitIterable = commitIterable; + this.converter = converter; + } + + @Override + public Iterator iterator() { + return new ConvertingChangesetIterator(commitIterable.iterator()); + } + + class ConvertingChangesetIterator implements Iterator { + + private final Iterator commitIterator; + + private ConvertingChangesetIterator(Iterator commitIterator) { + this.commitIterator = commitIterator; + } + + @Override + public boolean hasNext() { + return commitIterator.hasNext(); + } + + @Override + public Changeset next() { + return converter.createChangeset(commitIterator.next()); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitImportHookContextProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitImportHookContextProvider.java new file mode 100644 index 0000000000..c5cf350e5f --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitImportHookContextProvider.java @@ -0,0 +1,94 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import com.google.common.collect.ImmutableSet; +import sonia.scm.repository.GitChangesetConverter; +import sonia.scm.repository.Tag; +import sonia.scm.repository.api.HookBranchProvider; +import sonia.scm.repository.api.HookFeature; +import sonia.scm.repository.api.HookTagProvider; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +class GitImportHookContextProvider extends HookContextProvider { + private final GitChangesetConverter converter; + private final List newTags; + private final GitLazyChangesetResolver changesetResolver; + private final List newBranches; + + GitImportHookContextProvider(GitChangesetConverter converter, + List newBranches, + List newTags, + GitLazyChangesetResolver changesetResolver) { + this.converter = converter; + this.newTags = newTags; + this.changesetResolver = changesetResolver; + this.newBranches = newBranches; + } + + @Override + public Set getSupportedFeatures() { + return ImmutableSet.of(HookFeature.CHANGESET_PROVIDER, HookFeature.BRANCH_PROVIDER, HookFeature.TAG_PROVIDER); + } + + @Override + public HookTagProvider getTagProvider() { + return new HookTagProvider() { + @Override + public List getCreatedTags() { + return newTags; + } + + @Override + public List getDeletedTags() { + return Collections.emptyList(); + } + }; + } + + @Override + public HookBranchProvider getBranchProvider() { + return new HookBranchProvider() { + @Override + public List getCreatedOrModified() { + return newBranches; + } + + @Override + public List getDeletedOrClosed() { + return Collections.emptyList(); + } + }; + } + + @Override + public HookChangesetProvider getChangesetProvider() { + GitConvertingChangesetIterable changesets = new GitConvertingChangesetIterable(changesetResolver.call(), converter); + return r -> new HookChangesetResponse(changesets); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLazyChangesetResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLazyChangesetResolver.java new file mode 100644 index 0000000000..357390be93 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitLazyChangesetResolver.java @@ -0,0 +1,59 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.revwalk.RevCommit; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; + +import java.io.IOException; +import java.util.concurrent.Callable; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +class GitLazyChangesetResolver implements Callable> { + private final Repository repository; + private final Git git; + + public GitLazyChangesetResolver(Repository repository, Git git) { + this.repository = repository; + this.git = git; + } + + @Override + public Iterable call() { + try { + return git.log().all().call(); + } catch (IOException | GitAPIException e) { + throw new InternalRepositoryException( + entity(repository).build(), + "Could not resolve changesets for imported repository", + e + ); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactory.java new file mode 100644 index 0000000000..3a505140ef --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactory.java @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import sonia.scm.repository.GitChangesetConverter; +import sonia.scm.repository.GitChangesetConverterFactory; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.Tag; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.List; + +import static sonia.scm.repository.RepositoryHookType.POST_RECEIVE; + +class GitPostReceiveRepositoryHookEventFactory { + + private final HookContextFactory hookContextFactory; + private final GitChangesetConverterFactory changesetConverterFactory; + + @Inject + public GitPostReceiveRepositoryHookEventFactory(HookContextFactory hookContextFactory, GitChangesetConverterFactory changesetConverterFactory) { + this.hookContextFactory = hookContextFactory; + this.changesetConverterFactory = changesetConverterFactory; + } + + PostReceiveRepositoryHookEvent createEvent(GitContext gitContext, + List branches, + List tags, + GitLazyChangesetResolver changesetResolver + ) throws IOException { + GitChangesetConverter converter = changesetConverterFactory.create(gitContext.open()); + GitImportHookContextProvider contextProvider = new GitImportHookContextProvider(converter, branches, tags, changesetResolver); + HookContext context = hookContextFactory.createContext(contextProvider, gitContext.getRepository()); + RepositoryHookEvent repositoryHookEvent = new RepositoryHookEvent(context, gitContext.getRepository(), POST_RECEIVE); + return new PostReceiveRepositoryHookEvent(repositoryHookEvent); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java index 7d58f86e5b..73130e7880 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitPullCommand.java @@ -24,8 +24,6 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Iterables; @@ -41,15 +39,19 @@ import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitUtil; import sonia.scm.repository.Repository; +import sonia.scm.repository.Tag; import sonia.scm.repository.api.ImportFailedException; import sonia.scm.repository.api.PullResponse; import javax.inject.Inject; import java.io.File; import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; /** * @author Sebastian Sdorra @@ -57,39 +59,21 @@ import java.io.IOException; public class GitPullCommand extends AbstractGitPushOrPullCommand implements PullCommand { - /** - * Field description - */ private static final String REF_SPEC = "refs/heads/*:refs/heads/*"; + private static final Logger LOG = LoggerFactory.getLogger(GitPullCommand.class); + private final ScmEventBus eventBus; + private final GitPostReceiveRepositoryHookEventFactory eventFactory; - /** - * Field description - */ - private static final Logger logger = - LoggerFactory.getLogger(GitPullCommand.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * @param handler - * @param context - */ @Inject - public GitPullCommand(GitRepositoryHandler handler, GitContext context) { + public GitPullCommand(GitRepositoryHandler handler, + GitContext context, + ScmEventBus eventBus, + GitPostReceiveRepositoryHookEventFactory eventFactory) { super(handler, context); + this.eventBus = eventBus; + this.eventFactory = eventFactory; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * @param request - * @return - * @throws IOException - */ @Override public PullResponse pull(PullCommandRequest request) throws IOException { @@ -108,24 +92,17 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand } private PullResponse convert(Git git, FetchResult fetch) { - long counter = 0l; + long counter = 0; for (TrackingRefUpdate tru : fetch.getTrackingRefUpdates()) { counter += count(git, tru); } - logger.debug("received {} changesets by pull", counter); + LOG.debug("received {} changesets by pull", counter); return new PullResponse(counter); } - /** - * Method description - * - * @param git - * @param tru - * @return - */ private long count(Git git, TrackingRefUpdate tru) { long counter = 0; @@ -151,12 +128,12 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand counter += Iterables.size(commits); } - logger.trace("counting {} commits for ref update {}", counter, tru); + LOG.trace("counting {} commits for ref update {}", counter, tru); } catch (Exception ex) { - logger.error("could not count pushed/pulled changesets", ex); + LOG.error("could not count pushed/pulled changesets", ex); } } else { - logger.debug("do not count non branch ref update {}", tru); + LOG.debug("do not count non branch ref update {}", tru); } return counter; @@ -174,10 +151,10 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand Preconditions.checkArgument(sourceDirectory.exists(), "target repository directory does not exists"); - logger.debug("pull changes from {} to {}", + LOG.debug("pull changes from {} to {}", sourceDirectory.getAbsolutePath(), repository.getId()); - PullResponse response = null; + PullResponse response; org.eclipse.jgit.lib.Repository source = null; @@ -193,14 +170,14 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand private PullResponse pullFromUrl(PullCommandRequest request) throws IOException { - logger.debug("pull changes from {} to {}", request.getRemoteUrl(), repository); + LOG.debug("pull changes from {} to {}", request.getRemoteUrl(), repository); PullResponse response; Git git = Git.wrap(open()); - + FetchResult result; try { //J- - FetchResult result = git.fetch() + result = git.fetch() .setCredentialsProvider( new UsernamePasswordCredentialsProvider( Strings.nullToEmpty(request.getUsername()), @@ -223,6 +200,37 @@ public class GitPullCommand extends AbstractGitPushOrPullCommand ); } + firePostReceiveRepositoryHookEvent(git, result); + return response; } + + private void firePostReceiveRepositoryHookEvent(Git git, FetchResult result) { + try { + List branches = getBranchesFromFetchResult(result); + List tags = getTagsFromFetchResult(result); + GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git); + eventBus.post(eventFactory.createEvent(context, branches, tags, changesetResolver)); + } catch (IOException e) { + throw new ImportFailedException( + ContextEntry.ContextBuilder.entity(context.getRepository()).build(), + "Could not fire post receive repository hook event after unbundle", + e + ); + } + } + + private List getTagsFromFetchResult(FetchResult result) { + return result.getAdvertisedRefs().stream() + .filter(r -> r.getName().startsWith("refs/tags/")) + .map(r -> new Tag(r.getName().substring("refs/tags/".length()), r.getObjectId().getName())) + .collect(Collectors.toList()); + } + + private List getBranchesFromFetchResult(FetchResult result) { + return result.getAdvertisedRefs().stream() + .filter(r -> r.getName().startsWith("refs/heads/")) + .map(r -> r.getLeaf().getName().substring("refs/heads/".length())) + .collect(Collectors.toList()); + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index 8d94234dd6..5c51d0b928 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -168,7 +168,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider { @Override public UnbundleCommand getUnbundleCommand() { - return new GitUnbundleCommand(context); + return commandInjector.getInstance(GitUnbundleCommand.class); } @Override diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitUnbundleCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitUnbundleCommand.java index 6a930a9cc9..14ac835a3e 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitUnbundleCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitUnbundleCommand.java @@ -24,13 +24,23 @@ package sonia.scm.repository.spi; import com.google.common.io.ByteSource; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Ref; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.ContextEntry; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.Tag; +import sonia.scm.repository.api.ImportFailedException; import sonia.scm.repository.api.UnbundleResponse; +import javax.inject.Inject; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; import static sonia.scm.util.Archives.extractTar; @@ -39,13 +49,21 @@ public class GitUnbundleCommand extends AbstractGitCommand implements UnbundleCo private static final Logger LOG = LoggerFactory.getLogger(GitUnbundleCommand.class); - GitUnbundleCommand(GitContext context) { + private final ScmEventBus eventBus; + private final GitPostReceiveRepositoryHookEventFactory eventFactory; + + @Inject + GitUnbundleCommand(GitContext context, + ScmEventBus eventBus, + GitPostReceiveRepositoryHookEventFactory eventFactory) { super(context); + this.eventBus = eventBus; + this.eventFactory = eventFactory; } @Override public UnbundleResponse unbundle(UnbundleCommandRequest request) throws IOException { - ByteSource archive = checkNotNull(request.getArchive(),"archive is required"); + ByteSource archive = checkNotNull(request.getArchive(), "archive is required"); Path repositoryDir = context.getDirectory().toPath(); LOG.debug("archive repository {} to {}", repositoryDir, archive); @@ -54,9 +72,39 @@ public class GitUnbundleCommand extends AbstractGitCommand implements UnbundleCo } unbundleRepositoryFromRequest(request, repositoryDir); + firePostReceiveRepositoryHookEvent(); + return new UnbundleResponse(0); } + private void firePostReceiveRepositoryHookEvent() { + try { + Git git = Git.wrap(context.open()); + List branches = extractBranches(git); + List tags = extractTags(git); + GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(context.getRepository(), git); + eventBus.post(eventFactory.createEvent(context, branches, tags, changesetResolver)); + } catch (IOException | GitAPIException e) { + throw new ImportFailedException( + ContextEntry.ContextBuilder.entity(context.getRepository()).build(), + "Could not fire post receive repository hook event after unbundle", + e + ); + } + } + + private List extractTags(Git git) throws GitAPIException { + return git.tagList().call().stream() + .map(r -> new Tag(r.getName(), r.getObjectId().getName())) + .collect(Collectors.toList()); + } + + private List extractBranches(Git git) throws GitAPIException { + return git.branchList().call().stream() + .map(Ref::getName) + .collect(Collectors.toList()); + } + private void unbundleRepositoryFromRequest(UnbundleCommandRequest request, Path repositoryDir) throws IOException { extractTar(request.getArchive().openBufferedStream(), repositoryDir).run(); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java index 208d9a16bf..61de609842 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java @@ -28,7 +28,6 @@ package sonia.scm.repository.spi; import com.google.common.base.Charsets; import com.google.common.io.Files; -import com.google.inject.Provider; import org.eclipse.jgit.api.CommitCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -39,8 +38,8 @@ import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.rules.TemporaryFolder; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Changeset; -import sonia.scm.repository.GitChangesetConverterFactory; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.Repository; @@ -57,22 +56,18 @@ import static org.mockito.Mockito.when; //~--- JDK imports ------------------------------------------------------------ /** - * * @author Sebastian Sdorra */ -public class AbstractRemoteCommandTestBase -{ +public class AbstractRemoteCommandTestBase { /** * Method description * - * * @throws GitAPIException * @throws IOException */ @Before - public void setup() throws IOException, GitAPIException - { + public void setup() throws IOException, GitAPIException { incomingDirectory = tempFolder.newFile("incoming"); incomingDirectory.delete(); outgoingDirectory = tempFolder.newFile("outgoing"); @@ -84,6 +79,9 @@ public class AbstractRemoteCommandTestBase incoming = Git.init().setDirectory(incomingDirectory).setBare(false).call(); outgoing = Git.init().setDirectory(outgoingDirectory).setBare(false).call(); + eventBus = mock(ScmEventBus.class); + eventFactory = mock(GitPostReceiveRepositoryHookEventFactory.class); + handler = mock(GitRepositoryHandler.class); when(handler.getDirectory(incomingRepository.getId())).thenReturn( incomingDirectory); @@ -93,11 +91,9 @@ public class AbstractRemoteCommandTestBase /** * Method description - * */ @After - public void tearDownProtocol() - { + public void tearDownProtocol() { Transport.unregister(proto); } @@ -105,11 +101,9 @@ public class AbstractRemoteCommandTestBase /** * Method description - * */ @Before - public void setUpProtocol() - { + public void setUpProtocol() { // store reference to handle weak references proto = new ScmTransportProtocol(GitTestHelper::createConverterFactory, () -> null, () -> null); @@ -121,12 +115,10 @@ public class AbstractRemoteCommandTestBase /** * Method description * - * * @param expected * @param actual */ - protected void assertCommitsEquals(RevCommit expected, Changeset actual) - { + protected void assertCommitsEquals(RevCommit expected, Changeset actual) { assertEquals(expected.getId().name(), actual.getId()); assertEquals(expected.getAuthorIdent().getName(), actual.getAuthor().getName()); @@ -138,16 +130,12 @@ public class AbstractRemoteCommandTestBase /** * Method description * - * * @param git * @param message - * * @return - * * @throws GitAPIException */ - protected RevCommit commit(Git git, String message) throws GitAPIException - { + protected RevCommit commit(Git git, String message) throws GitAPIException { User trillian = UserTestData.createTrillian(); CommitCommand cc = git.commit(); @@ -160,18 +148,15 @@ public class AbstractRemoteCommandTestBase /** * Method description * - * * @param git * @param parent * @param name * @param content - * * @throws GitAPIException * @throws IOException */ protected void write(Git git, File parent, String name, String content) - throws IOException, GitAPIException - { + throws IOException, GitAPIException { File file = new File(parent, name); Files.write(content, file, Charsets.UTF_8); @@ -180,31 +165,52 @@ public class AbstractRemoteCommandTestBase //~--- fields --------------------------------------------------------------- - /** Field description */ + /** + * Field description + */ @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); - /** Field description */ + /** + * Field description + */ protected GitRepositoryHandler handler; - /** Field description */ + /** + * Field description + */ protected Repository incomingRepository; - /** Field description */ + /** + * Field description + */ protected Git incoming; - /** Field description */ + /** + * Field description + */ protected File incomingDirectory; - /** Field description */ + /** + * Field description + */ protected Git outgoing; - /** Field description */ + /** + * Field description + */ protected File outgoingDirectory; - /** Field description */ + /** + * Field description + */ protected Repository outgoingRepository; - /** Field description */ + /** + * Field description + */ private ScmTransportProtocol proto; + + protected ScmEventBus eventBus; + protected GitPostReceiveRepositoryHookEventFactory eventFactory; } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java index 270c548120..fedab56e9f 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java @@ -44,24 +44,20 @@ import static org.junit.Assert.assertNotNull; //~--- JDK imports ------------------------------------------------------------ /** - * * @author Sebastian Sdorra */ public class GitIncomingCommandTest - extends AbstractRemoteCommandTestBase -{ + extends AbstractRemoteCommandTestBase { /** * Method description * - * * @throws GitAPIException * @throws IOException */ @Test public void testGetIncomingChangesets() - throws IOException, GitAPIException - { + throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); RevCommit c1 = commit(outgoing, "added a"); @@ -84,22 +80,25 @@ public class GitIncomingCommandTest assertCommitsEquals(c2, cpr.getChangesets().get(1)); } - /** + /** * Method description * - * * @throws GitAPIException * @throws IOException */ @Test - public void testGetIncomingChangesetsWithAllreadyPullChangesets() - throws IOException, GitAPIException - { + public void testGetIncomingChangesetsWithAlreadyPulledChangesets() + throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); commit(outgoing, "added a"); - GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()), new GitConfig())); + GitPullCommand pull = new GitPullCommand( + handler, + new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()), new GitConfig()), + eventBus, + eventFactory + ); PullCommandRequest req = new PullCommandRequest(); req.setRemoteRepository(outgoingRepository); pull.pull(req); @@ -124,13 +123,11 @@ public class GitIncomingCommandTest /** * Method description * - * * @throws IOException */ @Test public void testGetIncomingChangesetsWithEmptyRepository() - throws IOException - { + throws IOException { GitIncomingCommand cmd = createCommand(); IncomingCommandRequest request = new IncomingCommandRequest(); @@ -146,15 +143,13 @@ public class GitIncomingCommandTest /** * Check for correct behaviour * - * * @throws GitAPIException * @throws IOException */ @Test @Ignore public void testGetIncomingChangesetsWithUnrelatedRepository() - throws IOException, GitAPIException - { + throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); commit(outgoing, "added a"); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLazyChangesetResolverTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLazyChangesetResolverTest.java new file mode 100644 index 0000000000..cfd75757f0 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLazyChangesetResolverTest.java @@ -0,0 +1,71 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import com.google.common.collect.Iterables; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; +import sonia.scm.repository.api.ImportFailedException; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +public class GitLazyChangesetResolverTest extends AbstractGitCommandTestBase { + + @Test + public void shouldResolveChangesets() throws IOException { + GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(repository, Git.wrap(createContext().open())); + Iterable commits = changesetResolver.call(); + + RevCommit firstCommit = commits.iterator().next(); + assertThat(firstCommit.getId().toString()).isEqualTo("commit a8495c0335a13e6e432df90b3727fa91943189a7 1602078219 -----sp"); + assertThat(firstCommit.getCommitTime()).isEqualTo(1602078219); + assertThat(firstCommit.getFullMessage()).isEqualTo("add deeper paths\n"); + } + + @Test + public void shouldResolveAllChangesets() throws IOException, GitAPIException { + Git git = Git.wrap(createContext().open()); + GitLazyChangesetResolver changesetResolver = new GitLazyChangesetResolver(repository, git); + Iterable allCommits = changesetResolver.call(); + int allCommitsCounter = Iterables.size(allCommits); + int singleBranchCommitsCounter = Iterables.size(git.log().call()); + + assertThat(allCommitsCounter).isGreaterThan(singleBranchCommitsCounter); + } + + @Test(expected = ImportFailedException.class) + public void shouldThrowImportFailedException() { + Git git = mock(Git.class); + doThrow(ImportFailedException.class).when(git).log(); + new GitLazyChangesetResolver(repository, git).call(); + } +} + diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java index 28ddd0a234..0cda0dc724 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java @@ -28,6 +28,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Before; import org.junit.Test; import sonia.scm.repository.GitConfig; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.Modifications; import java.io.File; @@ -111,7 +112,12 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase { PushCommandRequest request = new PushCommandRequest(); request.setRemoteRepository(incomingRepository); cmd.push(request); - GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, incomingRepository, null, new GitConfig())); + GitPullCommand pullCommand = new GitPullCommand( + handler, + new GitContext(incomingDirectory, incomingRepository, null, new GitConfig()), + eventBus, + eventFactory + ); PullCommandRequest pullRequest = new PullCommandRequest(); pullRequest.setRemoteRepository(incomingRepository); pullCommand.pull(pullRequest); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactoryTest.java new file mode 100644 index 0000000000..9aef4bffb7 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPostReceiveRepositoryHookEventFactoryTest.java @@ -0,0 +1,82 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import com.google.common.collect.ImmutableList; +import org.eclipse.jgit.api.Git; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.GitChangesetConverterFactory; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.Tag; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GitPostReceiveRepositoryHookEventFactoryTest extends AbstractGitCommandTestBase { + + private HookContext hookContext; + + private GitPostReceiveRepositoryHookEventFactory eventFactory; + + @Before + public void init() { + HookContextFactory hookContextFactory = mock(HookContextFactory.class); + hookContext = mock(HookContext.class, RETURNS_DEEP_STUBS); + when(hookContextFactory.createContext(any(), eq(repository))).thenReturn(hookContext); + GitChangesetConverterFactory converterFactory = mock(GitChangesetConverterFactory.class); + eventFactory = new GitPostReceiveRepositoryHookEventFactory(hookContextFactory, converterFactory); + } + + @Test + public void shouldCreateEvent() throws IOException { + ImmutableList branches = ImmutableList.of("master", "develop"); + ImmutableList tags = ImmutableList.of(new Tag("1.0", "123"), new Tag("2.0", "456")); + ImmutableList changesets = ImmutableList.of(new Changeset("1", 0L, null, "first")); + when(hookContext.getChangesetProvider().getChangesetList()).thenReturn(changesets); + when(hookContext.getBranchProvider().getCreatedOrModified()).thenReturn(branches); + when(hookContext.getTagProvider().getCreatedTags()).thenReturn(tags); + + PostReceiveRepositoryHookEvent event = eventFactory.createEvent( + createContext(), + branches, + tags, + new GitLazyChangesetResolver(repository, Git.wrap(createContext().open())) + ); + + assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).isSameAs(branches); + assertThat(event.getContext().getTagProvider().getCreatedTags()).isSameAs(tags); + assertThat(event.getContext().getChangesetProvider().getChangesetList().get(0).getId()).isEqualTo("1"); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitUnbundleCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitUnbundleCommandTest.java index 806e1c5167..0494b78bfb 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitUnbundleCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitUnbundleCommandTest.java @@ -24,70 +24,79 @@ package sonia.scm.repository.spi; import com.google.common.io.ByteSource; -import com.google.common.io.Files; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.RepositoryHookType; import sonia.scm.util.Archives; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -class GitUnbundleCommandTest extends AbstractGitCommandTestBase { - private GitContext gitContext; +public class GitUnbundleCommandTest extends AbstractGitCommandTestBase { + private ScmEventBus eventBus; private GitUnbundleCommand unbundleCommand; + private GitPostReceiveRepositoryHookEventFactory eventFactory; - @BeforeEach - void initCommand() { - gitContext = mock(GitContext.class); - unbundleCommand = new GitUnbundleCommand(gitContext); + @Before + public void initUnbundleCommand() { + eventBus = mock(ScmEventBus.class); + eventFactory = mock(GitPostReceiveRepositoryHookEventFactory.class); + unbundleCommand = new GitUnbundleCommand(createContext(), eventBus, eventFactory); } @Test - void shouldUnbundleRepositoryFiles(@TempDir Path temp) throws IOException { + public void shouldUnbundleRepositoryFiles() throws IOException { + when(eventFactory.createEvent(eq(createContext()), any(), any(), any())) + .thenReturn(new PostReceiveRepositoryHookEvent(new RepositoryHookEvent(null, repository, RepositoryHookType.POST_RECEIVE))); + String filePath = "test-input"; String fileContent = "HeartOfGold"; - UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(temp, filePath, fileContent); + UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(filePath, fileContent); unbundleCommand.unbundle(unbundleCommandRequest); - assertFileWithContentWasCreated(temp, filePath, fileContent); + assertFileWithContentWasCreated(createContext().getDirectory(), filePath, fileContent); + + verify(eventBus).post(any(PostReceiveRepositoryHookEvent.class)); } @Test - void shouldUnbundleNestedRepositoryFiles(@TempDir Path temp) throws IOException { + public void shouldUnbundleNestedRepositoryFiles() throws IOException { String filePath = "objects/pack/test-input"; String fileContent = "hitchhiker"; - UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(temp, filePath, fileContent); + UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(filePath, fileContent); unbundleCommand.unbundle(unbundleCommandRequest); - assertFileWithContentWasCreated(temp, filePath, fileContent); + assertFileWithContentWasCreated(createContext().getDirectory(), filePath, fileContent); } - private void assertFileWithContentWasCreated(@TempDir Path temp, String filePath, String fileContent) throws IOException { - File createdFile = temp.resolve(filePath).toFile(); + private void assertFileWithContentWasCreated(File temp, String filePath, String fileContent) throws IOException { + File createdFile = temp.toPath().resolve(filePath).toFile(); assertThat(createdFile).exists(); - assertThat(Files.readLines(createdFile, StandardCharsets.UTF_8).get(0)).isEqualTo(fileContent); + assertThat(createdFile).hasContent(fileContent); } - private UnbundleCommandRequest createUnbundleCommandRequestForFile(Path temp, String filePath, String fileContent) throws IOException { + private UnbundleCommandRequest createUnbundleCommandRequestForFile(String filePath, String fileContent) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); TarArchiveOutputStream taos = Archives.createTarOutputStream(baos); addEntry(taos, filePath, fileContent); taos.finish(); taos.close(); - when(gitContext.getDirectory()).thenReturn(temp.toFile()); ByteSource byteSource = ByteSource.wrap(baos.toByteArray()); UnbundleCommandRequest unbundleCommandRequest = new UnbundleCommandRequest(byteSource); return unbundleCommandRequest; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesTagsExtractor.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesTagsExtractor.java new file mode 100644 index 0000000000..345dccf451 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesTagsExtractor.java @@ -0,0 +1,50 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import com.aragost.javahg.Branch; +import sonia.scm.repository.Tag; + +import java.util.List; +import java.util.stream.Collectors; + +@SuppressWarnings("java:S3252") // This is just how to use javahg +final class HgBranchesTagsExtractor { + + private HgBranchesTagsExtractor() { + } + + static List extractTags(HgCommandContext context) { + return com.aragost.javahg.commands.TagsCommand.on(context.open()).execute().stream() + .map(t -> new Tag(t.getName(), t.getChangeset().toString())) + .collect(Collectors.toList()); + } + + static List extractBranches(HgCommandContext context) { + return com.aragost.javahg.commands.BranchesCommand.on(context.open()).execute().stream() + .map(Branch::getName) + .collect(Collectors.toList()); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgImportHookContextProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgImportHookContextProvider.java new file mode 100644 index 0000000000..432aeecb8c --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgImportHookContextProvider.java @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import com.google.common.collect.ImmutableSet; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Tag; +import sonia.scm.repository.api.HookBranchProvider; +import sonia.scm.repository.api.HookFeature; +import sonia.scm.repository.api.HookTagProvider; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +class HgImportHookContextProvider extends HookContextProvider { + private final List newBranches; + private final List newTags; + private final HgLazyChangesetResolver changesetResolver; + + HgImportHookContextProvider(List newBranches, List newTags, HgLazyChangesetResolver changesetResolver) { + this.newBranches = newBranches; + this.newTags = newTags; + this.changesetResolver = changesetResolver; + } + + @Override + public Set getSupportedFeatures() { + return ImmutableSet.of(HookFeature.CHANGESET_PROVIDER, HookFeature.BRANCH_PROVIDER, HookFeature.TAG_PROVIDER); + } + + @Override + public HookTagProvider getTagProvider() { + return new HookTagProvider() { + @Override + public List getCreatedTags() { + return newTags; + } + + @Override + public List getDeletedTags() { + return Collections.emptyList(); + } + }; + } + + @Override + public HookBranchProvider getBranchProvider() { + return new HookBranchProvider() { + @Override + public List getCreatedOrModified() { + return newBranches; + } + + @Override + public List getDeletedOrClosed() { + return Collections.emptyList(); + } + }; + } + + @Override + public HookChangesetProvider getChangesetProvider() { + Iterable changesets = changesetResolver.call(); + return r -> new HookChangesetResponse(changesets); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLazyChangesetResolver.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLazyChangesetResolver.java new file mode 100644 index 0000000000..d0a9cb80a3 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLazyChangesetResolver.java @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import com.aragost.javahg.commands.LogCommand; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.HgRepositoryFactory; +import sonia.scm.repository.Person; +import sonia.scm.repository.Repository; + +import java.util.Iterator; +import java.util.concurrent.Callable; + +class HgLazyChangesetResolver implements Callable> { + + private final HgRepositoryFactory factory; + private final Repository repository; + + HgLazyChangesetResolver(HgRepositoryFactory factory, Repository repository) { + this.factory = factory; + this.repository = repository; + } + + @Override + public Iterable call() { + Iterator iterator = LogCommand.on(factory.openForRead(repository)).execute().stream() + .map(changeset -> new Changeset( + changeset.getNode(), + changeset.getTimestamp().getDate().getTime(), + Person.toPerson(changeset.getUser()), + changeset.getMessage()) + ) + .iterator(); + return () -> iterator; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPostReceiveRepositoryHookEventFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPostReceiveRepositoryHookEventFactory.java new file mode 100644 index 0000000000..542253345a --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPostReceiveRepositoryHookEventFactory.java @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.Tag; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; + +import javax.inject.Inject; +import java.util.List; + +import static sonia.scm.repository.RepositoryHookType.POST_RECEIVE; +import static sonia.scm.repository.spi.HgBranchesTagsExtractor.extractBranches; +import static sonia.scm.repository.spi.HgBranchesTagsExtractor.extractTags; + +class HgPostReceiveRepositoryHookEventFactory { + + private final HookContextFactory hookContextFactory; + + @Inject + public HgPostReceiveRepositoryHookEventFactory(HookContextFactory hookContextFactory) { + this.hookContextFactory = hookContextFactory; + } + + PostReceiveRepositoryHookEvent createEvent(HgCommandContext hgContext, HgLazyChangesetResolver changesetResolver) { + List branches = extractBranches(hgContext); + List tags = extractTags(hgContext); + HgImportHookContextProvider contextProvider = new HgImportHookContextProvider(branches, tags, changesetResolver); + HookContext context = hookContextFactory.createContext(contextProvider, hgContext.getScmRepository()); + RepositoryHookEvent repositoryHookEvent = new RepositoryHookEvent(context, hgContext.getScmRepository(), POST_RECEIVE); + return new PostReceiveRepositoryHookEvent(repositoryHookEvent); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java index 6c75e1924e..ba48592263 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgPullCommand.java @@ -30,6 +30,7 @@ import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; +import sonia.scm.event.ScmEventBus; import sonia.scm.io.INIConfiguration; import sonia.scm.io.INIConfigurationReader; import sonia.scm.io.INIConfigurationWriter; @@ -47,9 +48,20 @@ public class HgPullCommand extends AbstractHgPushOrPullCommand implements PullCo private static final Logger LOG = LoggerFactory.getLogger(HgPullCommand.class); private static final String AUTH_SECTION = "auth"; + private final ScmEventBus eventBus; + private final HgLazyChangesetResolver changesetResolver; + private final HgPostReceiveRepositoryHookEventFactory eventFactory; - public HgPullCommand(HgRepositoryHandler handler, HgCommandContext context) { + public HgPullCommand(HgRepositoryHandler handler, + HgCommandContext context, + ScmEventBus eventBus, + HgLazyChangesetResolver changesetResolver, + HgPostReceiveRepositoryHookEventFactory eventFactory + ) { super(handler, context); + this.eventBus = eventBus; + this.changesetResolver = changesetResolver; + this.eventFactory = eventFactory; } @Override @@ -74,9 +86,15 @@ public class HgPullCommand extends AbstractHgPushOrPullCommand implements PullCo removeAuthenticationConfig(); } + firePostReceiveRepositoryHookEvent(); + return new PullResponse(result.size()); } + private void firePostReceiveRepositoryHookEvent() { + eventBus.post(eventFactory.createEvent(context, changesetResolver)); + } + public void addAuthenticationConfig(PullCommandRequest request, String url) throws IOException { INIConfiguration ini = readIniConfiguration(); INISection authSection = ini.getSection(AUTH_SECTION); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java index a766f35fe3..2fe276a3d6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceProvider.java @@ -25,6 +25,7 @@ package sonia.scm.repository.spi; import com.google.common.io.Closeables; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Feature; import sonia.scm.repository.HgRepositoryFactory; import sonia.scm.repository.HgRepositoryHandler; @@ -37,7 +38,6 @@ import java.util.EnumSet; import java.util.Set; /** - * * @author Sebastian Sdorra */ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { @@ -65,49 +65,34 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { private final HgRepositoryHandler handler; private final HgCommandContext context; + private final HgLazyChangesetResolver lazyChangesetResolver; + private final HgPostReceiveRepositoryHookEventFactory eventFactory; + private final ScmEventBus eventBus; - HgRepositoryServiceProvider(HgRepositoryHandler handler, HgRepositoryFactory factory, Repository repository) { + HgRepositoryServiceProvider(HgRepositoryHandler handler, + HgRepositoryFactory factory, + HgPostReceiveRepositoryHookEventFactory eventFactory, + ScmEventBus eventBus, + Repository repository) { this.handler = handler; + this.eventBus = eventBus; + this.eventFactory = eventFactory; this.context = new HgCommandContext(handler, factory, repository); + this.lazyChangesetResolver = new HgLazyChangesetResolver(factory, repository); } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @throws IOException - */ @Override - public void close() throws IOException - { + public void close() throws IOException { Closeables.close(context, true); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ @Override - public HgBlameCommand getBlameCommand() - { + public HgBlameCommand getBlameCommand() { return new HgBlameCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public BranchesCommand getBranchesCommand() - { + public BranchesCommand getBranchesCommand() { return new HgBranchesCommand(context); } @@ -116,63 +101,28 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { return new HgBranchCommand(context, handler.getWorkingCopyFactory()); } - /** - * Method description - * - * - * @return - */ @Override - public HgBrowseCommand getBrowseCommand() - { + public HgBrowseCommand getBrowseCommand() { return new HgBrowseCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public HgCatCommand getCatCommand() - { + public HgCatCommand getCatCommand() { return new HgCatCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public HgDiffCommand getDiffCommand() - { + public HgDiffCommand getDiffCommand() { return new HgDiffCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public IncomingCommand getIncomingCommand() - { + public IncomingCommand getIncomingCommand() { return new HgIncomingCommand(context, handler); } - /** - * Method description - * - * - * @return - */ @Override - public HgLogCommand getLogCommand() - { + public HgLogCommand getLogCommand() { return new HgLogCommand(context); } @@ -187,39 +137,18 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { return new HgModificationsCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public OutgoingCommand getOutgoingCommand() - { + public OutgoingCommand getOutgoingCommand() { return new HgOutgoingCommand(context, handler); } - /** - * Method description - * - * - * @return - */ @Override - public PullCommand getPullCommand() - { - return new HgPullCommand(handler, context); + public PullCommand getPullCommand() { + return new HgPullCommand(handler, context, eventBus, lazyChangesetResolver, eventFactory); } - /** - * Method description - * - * - * @return - */ @Override - public PushCommand getPushCommand() - { + public PushCommand getPushCommand() { return new HgPushCommand(handler, context); } @@ -228,39 +157,18 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { return new HgModifyCommand(context, handler.getWorkingCopyFactory()); } - /** - * Method description - * - * - * @return - */ @Override - public Set getSupportedCommands() - { + public Set getSupportedCommands() { return COMMANDS; } - /** - * Method description - * - * - * @return - */ @Override - public Set getSupportedFeatures() - { + public Set getSupportedFeatures() { return FEATURES; } - /** - * Method description - * - * - * @return - */ @Override - public TagsCommand getTagsCommand() - { + public TagsCommand getTagsCommand() { return new HgTagsCommand(context); } @@ -276,6 +184,6 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider { @Override public UnbundleCommand getUnbundleCommand() { - return new HgUnbundleCommand(context); + return new HgUnbundleCommand(context, eventBus, lazyChangesetResolver, eventFactory); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java index 87a5d6d6fb..3fd7f83378 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgRepositoryServiceResolver.java @@ -25,13 +25,13 @@ package sonia.scm.repository.spi; import com.google.inject.Inject; +import sonia.scm.event.ScmEventBus; import sonia.scm.plugin.Extension; import sonia.scm.repository.HgRepositoryFactory; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.Repository; /** - * * @author Sebastian Sdorra */ @Extension @@ -39,11 +39,19 @@ public class HgRepositoryServiceResolver implements RepositoryServiceResolver { private final HgRepositoryHandler handler; private final HgRepositoryFactory factory; + private final ScmEventBus eventBus; + private final HgPostReceiveRepositoryHookEventFactory eventFactory; @Inject - public HgRepositoryServiceResolver(HgRepositoryHandler handler, HgRepositoryFactory factory) { + public HgRepositoryServiceResolver(HgRepositoryHandler handler, + HgRepositoryFactory factory, + ScmEventBus eventBus, + HgPostReceiveRepositoryHookEventFactory eventFactory + ) { this.handler = handler; this.factory = factory; + this.eventBus = eventBus; + this.eventFactory = eventFactory; } @Override @@ -51,7 +59,7 @@ public class HgRepositoryServiceResolver implements RepositoryServiceResolver { HgRepositoryServiceProvider provider = null; if (HgRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new HgRepositoryServiceProvider(handler, factory, repository); + provider = new HgRepositoryServiceProvider(handler, factory, eventFactory, eventBus, repository); } return provider; diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java index e2c2d3dd54..2a6aacad97 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgUnbundleCommand.java @@ -27,6 +27,7 @@ package sonia.scm.repository.spi; import com.google.common.io.ByteSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.api.UnbundleResponse; import java.io.IOException; @@ -36,19 +37,28 @@ import java.nio.file.Path; import static com.google.common.base.Preconditions.checkNotNull; import static sonia.scm.util.Archives.extractTar; - -public class HgUnbundleCommand implements UnbundleCommand { +public class HgUnbundleCommand implements UnbundleCommand { private static final Logger LOG = LoggerFactory.getLogger(HgUnbundleCommand.class); private final HgCommandContext context; + private final ScmEventBus eventBus; + private final HgLazyChangesetResolver changesetResolver; + private final HgPostReceiveRepositoryHookEventFactory eventFactory; - HgUnbundleCommand(HgCommandContext context) { + HgUnbundleCommand(HgCommandContext context, + ScmEventBus eventBus, + HgLazyChangesetResolver changesetResolver, + HgPostReceiveRepositoryHookEventFactory eventFactory + ) { this.context = context; + this.eventBus = eventBus; + this.changesetResolver = changesetResolver; + this.eventFactory = eventFactory; } @Override public UnbundleResponse unbundle(UnbundleCommandRequest request) throws IOException { - ByteSource archive = checkNotNull(request.getArchive(),"archive is required"); + ByteSource archive = checkNotNull(request.getArchive(), "archive is required"); Path repositoryDir = context.getDirectory().toPath(); LOG.debug("archive repository {} to {}", repositoryDir, archive); @@ -57,9 +67,14 @@ public class HgUnbundleCommand implements UnbundleCommand { } unbundleRepositoryFromRequest(request, repositoryDir); + firePostReceiveRepositoryHookEvent(); return new UnbundleResponse(0); } + private void firePostReceiveRepositoryHookEvent() { + eventBus.post(eventFactory.createEvent(context, changesetResolver)); + } + private void unbundleRepositoryFromRequest(UnbundleCommandRequest request, Path repositoryDir) throws IOException { extractTar(request.getArchive().openBufferedStream(), repositoryDir).run(); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLazyChangesetResolverTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLazyChangesetResolverTest.java new file mode 100644 index 0000000000..f881b665d5 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLazyChangesetResolverTest.java @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import org.junit.Test; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.HgTestUtil; +import sonia.scm.repository.Person; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HgLazyChangesetResolverTest extends AbstractHgCommandTestBase { + + @Test + public void shouldResolveChangesets() { + HgLazyChangesetResolver changesetResolver = new HgLazyChangesetResolver(HgTestUtil.createFactory(handler, repositoryDirectory), repository); + Iterable changesets = changesetResolver.call(); + + Changeset firstChangeset = changesets.iterator().next(); + assertThat(firstChangeset.getId()).isEqualTo("2baab8e80280ef05a9aa76c49c76feca2872afb7"); + assertThat(firstChangeset.getDate()).isEqualTo(1339586381000L); + assertThat(firstChangeset.getAuthor()).isEqualTo(Person.toPerson("Zaphod Beeblebrox ")); + assertThat(firstChangeset.getDescription()).isEqualTo("added new line for blame"); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgPostReceiveRepositoryHookEventFactoryTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgPostReceiveRepositoryHookEventFactoryTest.java new file mode 100644 index 0000000000..7851823c34 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgPostReceiveRepositoryHookEventFactoryTest.java @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * 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. + */ + +package sonia.scm.repository.spi; + +import com.google.common.collect.ImmutableList; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.Tag; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class HgPostReceiveRepositoryHookEventFactoryTest extends AbstractHgCommandTestBase { + + private HookContext hookContext; + + private HgPostReceiveRepositoryHookEventFactory eventFactory; + + @Before + public void init() { + HookContextFactory hookContextFactory = mock(HookContextFactory.class); + hookContext = mock(HookContext.class, RETURNS_DEEP_STUBS); + when(hookContextFactory.createContext(any(), any())).thenReturn(hookContext); + eventFactory = new HgPostReceiveRepositoryHookEventFactory(hookContextFactory); + } + + @Test + public void shouldCreateEvent() { + ImmutableList branches = ImmutableList.of("master", "develop"); + ImmutableList tags = ImmutableList.of(new Tag("1.0", "123"), new Tag("2.0", "456")); + ImmutableList changesets = ImmutableList.of(new Changeset("1", 0L, null, "first")); + when(hookContext.getChangesetProvider().getChangesetList()).thenReturn(changesets); + when(hookContext.getBranchProvider().getCreatedOrModified()).thenReturn(branches); + when(hookContext.getTagProvider().getCreatedTags()).thenReturn(tags); + + HgLazyChangesetResolver changesetResolver = mock(HgLazyChangesetResolver.class); + + PostReceiveRepositoryHookEvent event = eventFactory.createEvent(cmdContext, changesetResolver); + + assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).isSameAs(branches); + assertThat(event.getContext().getTagProvider().getCreatedTags()).isSameAs(tags); + assertThat(event.getContext().getChangesetProvider().getChangesetList().get(0).getId()).isEqualTo("1"); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java index 9343b46c1c..a9f53bd4b9 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgUnbundleCommandTest.java @@ -28,70 +28,82 @@ import com.google.common.io.ByteSource; import com.google.common.io.Files; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.HgTestUtil; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.RepositoryHookType; import sonia.scm.util.Archives; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -class HgUnbundleCommandTest { - private HgCommandContext hgContext; - private HgUnbundleCommand unbundleCommand; +public class HgUnbundleCommandTest extends AbstractHgCommandTestBase { - @BeforeEach - void initCommand() { - hgContext = mock(HgCommandContext.class); - unbundleCommand = new HgUnbundleCommand(hgContext); + private ScmEventBus eventBus; + private HgUnbundleCommand unbundleCommand; + private HgPostReceiveRepositoryHookEventFactory eventFactory; + + @Before + public void initUnbundleCommand() { + eventBus = mock(ScmEventBus.class); + eventFactory = mock(HgPostReceiveRepositoryHookEventFactory.class); + unbundleCommand = new HgUnbundleCommand(cmdContext, eventBus, new HgLazyChangesetResolver(HgTestUtil.createFactory(handler, repositoryDirectory), null), eventFactory); } @Test - void shouldUnbundleRepositoryFiles(@TempDir Path temp) throws IOException { + public void shouldUnbundleRepositoryFiles() throws IOException { + when(eventFactory.createEvent(eq(cmdContext), any())) + .thenReturn(new PostReceiveRepositoryHookEvent(new RepositoryHookEvent(null, repository, RepositoryHookType.POST_RECEIVE))); + + String filePath = "test-input"; String fileContent = "HeartOfGold"; - UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(temp, filePath, fileContent); + UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(filePath, fileContent); unbundleCommand.unbundle(unbundleCommandRequest); - assertFileWithContentWasCreated(temp, filePath, fileContent); + assertFileWithContentWasCreated(cmdContext.getDirectory(), filePath, fileContent); + verify(eventBus).post(any(PostReceiveRepositoryHookEvent.class)); } @Test - void shouldUnbundleNestedRepositoryFiles(@TempDir Path temp) throws IOException { + public void shouldUnbundleNestedRepositoryFiles() throws IOException { String filePath = "objects/pack/test-input"; String fileContent = "hitchhiker"; - UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(temp, filePath, fileContent); + UnbundleCommandRequest unbundleCommandRequest = createUnbundleCommandRequestForFile(filePath, fileContent); unbundleCommand.unbundle(unbundleCommandRequest); - assertFileWithContentWasCreated(temp, filePath, fileContent); + assertFileWithContentWasCreated(cmdContext.getDirectory(), filePath, fileContent); } - private void assertFileWithContentWasCreated(@TempDir Path temp, String filePath, String fileContent) throws IOException { - File createdFile = temp.resolve(filePath).toFile(); + private void assertFileWithContentWasCreated(File temp, String filePath, String fileContent) throws IOException { + File createdFile = temp.toPath().resolve(filePath).toFile(); assertThat(createdFile).exists(); assertThat(Files.readLines(createdFile, StandardCharsets.UTF_8).get(0)).isEqualTo(fileContent); } - private UnbundleCommandRequest createUnbundleCommandRequestForFile(Path temp, String filePath, String fileContent) throws IOException { + private UnbundleCommandRequest createUnbundleCommandRequestForFile(String filePath, String fileContent) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); TarArchiveOutputStream taos = Archives.createTarOutputStream(baos); addEntry(taos, filePath, fileContent); taos.finish(); taos.close(); - when(hgContext.getDirectory()).thenReturn(temp.toFile()); ByteSource byteSource = ByteSource.wrap(baos.toByteArray()); - UnbundleCommandRequest unbundleCommandRequest = new UnbundleCommandRequest(byteSource); - return unbundleCommandRequest; + return new UnbundleCommandRequest(byteSource); } private void addEntry(TarArchiveOutputStream taos, String name, String input) throws IOException { diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java index d613f9284d..a31ae9efb2 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java @@ -24,8 +24,6 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Lists; @@ -46,49 +44,34 @@ import java.util.List; import static sonia.scm.repository.SvnUtil.parseRevision; -//~--- JDK imports ------------------------------------------------------------ +public class SvnLogCommand extends AbstractSvnCommand implements LogCommand { -public class SvnLogCommand extends AbstractSvnCommand implements LogCommand -{ + private static final Logger LOG = LoggerFactory.getLogger(SvnLogCommand.class); - /** - * the logger for SvnLogCommand - */ - private static final Logger logger = - LoggerFactory.getLogger(SvnLogCommand.class); - - SvnLogCommand(SvnContext context) - { + SvnLogCommand(SvnContext context) { super(context); } - //~--- get methods ---------------------------------------------------------- - @Override @SuppressWarnings("unchecked") public Changeset getChangeset(String revision, LogCommandRequest request) { Changeset changeset = null; - if (logger.isDebugEnabled()) - { - logger.debug("fetch changeset {}", revision); + if (LOG.isDebugEnabled()) { + LOG.debug("fetch changeset {}", revision); } - try - { + try { long revisioNumber = parseRevision(revision, repository); Preconditions.checkArgument(revisioNumber > 0, "revision must be greater than zero: %d", revisioNumber); SVNRepository repo = open(); Collection entries = repo.log(null, null, revisioNumber, - revisioNumber, true, true); + revisioNumber, true, true); - if (Util.isNotEmpty(entries)) - { + if (Util.isNotEmpty(entries)) { changeset = SvnUtil.createChangeset(entries.iterator().next()); } - } - catch (SVNException ex) - { + } catch (SVNException ex) { throw new InternalRepositoryException(repository, "could not open repository", ex); } @@ -98,73 +81,48 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand @Override @SuppressWarnings("unchecked") public ChangesetPagingResult getChangesets(LogCommandRequest request) { - if (logger.isDebugEnabled()) - { - logger.debug("fetch changesets for {}", request); + if (LOG.isDebugEnabled()) { + LOG.debug("fetch changesets for {}", request); } - ChangesetPagingResult changesets = null; + ChangesetPagingResult changesets; int start = request.getPagingStart(); int limit = request.getPagingLimit(); long startRevision = parseRevision(request.getStartChangeset(), repository); long endRevision = parseRevision(request.getEndChangeset(), repository); String[] pathArray = null; - if (!Strings.isNullOrEmpty(request.getPath())) - { - pathArray = new String[] { request.getPath() }; + if (!Strings.isNullOrEmpty(request.getPath())) { + pathArray = new String[]{request.getPath()}; } - try - { + try { SVNRepository repo = open(); - if ((startRevision > 0) || (pathArray != null)) - { + if ((startRevision > 0) || (pathArray != null)) { changesets = getChangesets(repo, startRevision, endRevision, start, limit, pathArray); - } - else - { + } else { changesets = getChangesets(repo, start, limit); } - } - catch (SVNException ex) - { + } catch (SVNException ex) { throw new InternalRepositoryException(repository, "could not open repository", ex); } return changesets; } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param repo - * @param start - * @param limit - * - * @return - * - * @throws SVNException - */ private ChangesetPagingResult getChangesets(SVNRepository repo, int start, - int limit) - throws SVNException - { + int limit) + throws SVNException { long latest = repo.getLatestRevision(); long startRev = latest - start; long endRev = Math.max(startRev - (limit - 1), 1); final List changesets = Lists.newArrayList(); - if (startRev > 0) - { - logger.debug("fetch changeset from {} to {}", startRev, endRev); + if (startRev > 0) { + LOG.debug("fetch changeset from {} to {}", startRev, endRev); repo.log(null, startRev, endRev, true, true, new ChangesetCollector(changesets)); } @@ -172,51 +130,30 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand return new ChangesetPagingResult((int) latest, changesets); } - /** - * Method description - * - * - * @param repo - * @param startRevision - * @param endRevision - * @param start - * @param limit - * @param path - * - * @return - * - * @throws SVNException - */ @SuppressWarnings("unchecked") private ChangesetPagingResult getChangesets(SVNRepository repo, - long startRevision, long endRevision, int start, int limit, String[] path) - throws SVNException - { + long startRevision, long endRevision, int start, int limit, String[] path) + throws SVNException { long startRev; long endRev = Math.max(endRevision, 0); long maxRev = repo.getLatestRevision(); - if (startRevision >= 0l) - { + if (startRevision >= 0) { startRev = startRevision; - } - else - { + } else { startRev = maxRev; } List changesetList = Lists.newArrayList(); - logger.debug("fetch changeset from {} to {} for path {}", startRev, endRev, + LOG.debug("fetch changeset from {} to {} for path {}", startRev, endRev, path); Collection entries = repo.log(path, null, startRev, endRev, - true, true); + true, true); - for (SVNLogEntry entry : entries) - { - if (entry.getRevision() <= maxRev) - { + for (SVNLogEntry entry : entries) { + if (entry.getRevision() <= maxRev) { changesetList.add(entry); } } @@ -225,17 +162,15 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand int max = limit + start; int end = total; - if ((max > 0) && (end > max)) - { + if ((max > 0) && (end > max)) { end = max; } - if (start < 0) - { + if (start < 0) { start = 0; } - logger.trace( + LOG.trace( "create sublist from {} to {} of total {} collected changesets", start, end, total); @@ -245,45 +180,20 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand SvnUtil.createChangesets(changesetList)); } - //~--- inner classes -------------------------------------------------------- - /** * Collect and convert changesets. - * */ - private static class ChangesetCollector implements ISVNLogEntryHandler - { + private static class ChangesetCollector implements ISVNLogEntryHandler { - /** - * Constructs ... - * - * - * @param changesets - */ - public ChangesetCollector(Collection changesets) - { + private final Collection changesets; + + public ChangesetCollector(Collection changesets) { this.changesets = changesets; } - //~--- methods ------------------------------------------------------------ - - /** - * Method description - * - * - * @param logEntry - * - * @throws SVNException - */ @Override - public void handleLogEntry(SVNLogEntry logEntry) throws SVNException - { + public void handleLogEntry(SVNLogEntry logEntry) { changesets.add(SvnUtil.createChangeset(logEntry)); } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private final Collection changesets; } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java index f948a7168f..e6d89f419d 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java @@ -26,23 +26,21 @@ package sonia.scm.repository.spi; import com.google.common.collect.ImmutableSet; import com.google.common.io.Closeables; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.repository.SvnWorkingCopyFactory; import sonia.scm.repository.api.Command; +import sonia.scm.repository.api.HookContextFactory; -import javax.inject.Inject; import java.io.IOException; import java.util.Set; /** - * * @author Sebastian Sdorra */ -public class SvnRepositoryServiceProvider extends RepositoryServiceProvider -{ +public class SvnRepositoryServiceProvider extends RepositoryServiceProvider { - /** Field description */ //J- public static final Set COMMANDS = ImmutableSet.of( Command.BLAME, Command.BROWSE, Command.CAT, Command.DIFF, @@ -50,101 +48,55 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider ); //J+ - //~--- constructors --------------------------------------------------------- - @Inject + private final SvnContext context; + private final SvnWorkingCopyFactory workingCopyFactory; + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; + SvnRepositoryServiceProvider(SvnRepositoryHandler handler, - Repository repository, SvnWorkingCopyFactory workingCopyFactory) - { + Repository repository, + SvnWorkingCopyFactory workingCopyFactory, + HookContextFactory hookContextFactory, + ScmEventBus eventBus) { this.context = new SvnContext(repository, handler.getDirectory(repository.getId())); this.workingCopyFactory = workingCopyFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @throws IOException - */ @Override - public void close() throws IOException - { + public void close() throws IOException { Closeables.close(context, true); } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ @Override - public SvnBlameCommand getBlameCommand() - { + public SvnBlameCommand getBlameCommand() { return new SvnBlameCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public SvnBrowseCommand getBrowseCommand() - { + public SvnBrowseCommand getBrowseCommand() { return new SvnBrowseCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public BundleCommand getBundleCommand() - { + public BundleCommand getBundleCommand() { return new SvnBundleCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public SvnCatCommand getCatCommand() - { + public SvnCatCommand getCatCommand() { return new SvnCatCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public SvnDiffCommand getDiffCommand() - { + public SvnDiffCommand getDiffCommand() { return new SvnDiffCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public SvnLogCommand getLogCommand() - { + public SvnLogCommand getLogCommand() { return new SvnLogCommand(context); } @@ -163,34 +115,13 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider return new SvnLookupCommand(context); } - /** - * Method description - * - * - * @return - */ @Override - public Set getSupportedCommands() - { + public Set getSupportedCommands() { return COMMANDS; } - /** - * Method description - * - * - * @return - */ @Override - public UnbundleCommand getUnbundleCommand() - { - return new SvnUnbundleCommand(context); + public UnbundleCommand getUnbundleCommand() { + return new SvnUnbundleCommand(context, hookContextFactory, eventBus, new SvnLogCommand(context)); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final SvnContext context; - - private final SvnWorkingCopyFactory workingCopyFactory; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java index 0a5bff15b7..9c66bd237b 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java @@ -25,21 +25,31 @@ package sonia.scm.repository.spi; import com.google.inject.Inject; +import sonia.scm.event.ScmEventBus; import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.repository.SvnWorkingCopyFactory; +import sonia.scm.repository.api.HookContextFactory; @Extension public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { - private SvnRepositoryHandler handler; - private SvnWorkingCopyFactory workingCopyFactory; + private final SvnRepositoryHandler handler; + private final SvnWorkingCopyFactory workingCopyFactory; + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; @Inject - public SvnRepositoryServiceResolver(SvnRepositoryHandler handler, SvnWorkingCopyFactory workingCopyFactory) { + public SvnRepositoryServiceResolver(SvnRepositoryHandler handler, + SvnWorkingCopyFactory workingCopyFactory, + HookContextFactory hookContextFactory, + ScmEventBus eventBus + ) { this.handler = handler; this.workingCopyFactory = workingCopyFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; } @Override @@ -47,7 +57,7 @@ public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { SvnRepositoryServiceProvider provider = null; if (SvnRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new SvnRepositoryServiceProvider(handler, repository, workingCopyFactory); + provider = new SvnRepositoryServiceProvider(handler, repository, workingCopyFactory, hookContextFactory, eventBus); } return provider; diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnUnbundleCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnUnbundleCommand.java index 973c93fea3..5bc2cd92bf 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnUnbundleCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnUnbundleCommand.java @@ -24,82 +24,74 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteSource; import com.google.common.io.Closeables; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.admin.SVNAdminClient; - +import sonia.scm.ContextEntry; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryHookEvent; import sonia.scm.repository.SvnUtil; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.repository.api.HookFeature; +import sonia.scm.repository.api.ImportFailedException; import sonia.scm.repository.api.UnbundleResponse; -import static com.google.common.base.Preconditions.*; - -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.Callable; + +import static com.google.common.base.Preconditions.checkNotNull; +import static sonia.scm.repository.RepositoryHookType.POST_RECEIVE; /** - * * @author Sebastian Sdorra */ -public class SvnUnbundleCommand extends AbstractSvnCommand - implements UnbundleCommand -{ +public class SvnUnbundleCommand extends AbstractSvnCommand implements UnbundleCommand { - /** Field description */ - private static final Logger logger = - LoggerFactory.getLogger(SvnUnbundleCommand.class); + private static final Logger LOG = LoggerFactory.getLogger(SvnUnbundleCommand.class); + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; + private final SvnLogCommand svnLogCommand; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * @param context - * - */ - public SvnUnbundleCommand(SvnContext context) - { + public SvnUnbundleCommand(SvnContext context, + HookContextFactory hookContextFactory, + ScmEventBus eventBus, + SvnLogCommand svnLogCommand + ) { super(context); + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; + this.svnLogCommand = svnLogCommand; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * - * @return - * - * @throws IOException - */ @Override public UnbundleResponse unbundle(UnbundleCommandRequest request) - throws IOException - { + throws IOException { ByteSource archive = checkNotNull(request.getArchive(), - "archive is required"); + "archive is required"); - logger.debug("archive repository {} to {}", context.getDirectory(), + LOG.debug("archive repository {} to {}", context.getDirectory(), archive); UnbundleResponse response; SVNClientManager clientManager = null; - try - { + try { clientManager = SVNClientManager.newInstance(); SVNAdminClient adminClient = clientManager.getAdminClient(); @@ -107,44 +99,124 @@ public class SvnUnbundleCommand extends AbstractSvnCommand restore(adminClient, archive, context.getDirectory()); response = new UnbundleResponse(context.open().getLatestRevision()); - } - catch (SVNException ex) - { + } catch (SVNException ex) { throw new IOException("could not restore dump", ex); - } - finally - { + } finally { SvnUtil.dispose(clientManager); } + firePostReceiveRepositoryHookEvent(); return response; } - /** - * Method description - * - * - * @param adminClient - * @param dump - * @param repository - * - * @throws IOException - * @throws SVNException - */ - private void restore(SVNAdminClient adminClient, ByteSource dump, - File repository) - throws SVNException, IOException - { + private void firePostReceiveRepositoryHookEvent() { + eventBus.post(createEvent()); + } + + private PostReceiveRepositoryHookEvent createEvent() { + Repository repository = this.context.getRepository(); + HookContext context = hookContextFactory.createContext(new SvnImportHookContextProvider(repository, svnLogCommand), repository); + RepositoryHookEvent repositoryHookEvent = new RepositoryHookEvent(context, repository, POST_RECEIVE); + return new PostReceiveRepositoryHookEvent(repositoryHookEvent); + } + + private void restore(SVNAdminClient adminClient, ByteSource dump, File repository) throws SVNException, IOException { InputStream inputStream = null; - try - { + try { inputStream = dump.openBufferedStream(); adminClient.doLoad(repository, inputStream); - } - finally - { + } finally { Closeables.close(inputStream, true); } } + + private static class SvnImportHookContextProvider extends HookContextProvider { + + private final Repository repository; + private final LogCommand logCommand; + + private SvnImportHookContextProvider(Repository repository, SvnLogCommand logCommand) { + this.repository = repository; + this.logCommand = logCommand; + } + + @Override + public Set getSupportedFeatures() { + return ImmutableSet.of(HookFeature.CHANGESET_PROVIDER); + } + + @Override + public HookChangesetProvider getChangesetProvider() { + ChangesetResolver changesetResolver = new ChangesetResolver(repository, logCommand); + return r -> new HookChangesetResponse(changesetResolver.call()); + } + } + + private static class ChangesetResolver implements Callable> { + + private final Repository repository; + private final LogCommand logCommand; + + ChangesetResolver(Repository repository, LogCommand logCommand) { + this.repository = repository; + this.logCommand = logCommand; + } + + @Override + public Iterable call() { + return SingleLogRequestChangesetIterator::new; + } + + private class SingleLogRequestChangesetIterator implements Iterator { + + private int currentNumber = 0; + private Changeset nextChangeset; + + SingleLogRequestChangesetIterator() { + prefetch(); + } + + @Override + public boolean hasNext() { + return nextChangeset != null; + } + + @Override + public Changeset next() { + if (nextChangeset == null) { + throw new NoSuchElementException(); + } + Changeset currentChangeset = nextChangeset; + prefetch(); + return currentChangeset; + } + + private void prefetch() { + try { + List changesets = fetchSingleChangesetPage(); + if (changesets.isEmpty()) { + nextChangeset = null; + } else { + nextChangeset = changesets.get(0); + } + } catch (IOException e) { + throw new ImportFailedException( + ContextEntry.ContextBuilder.entity(repository).build(), + "Could not provide changeset nr " + currentNumber + " for imported repository", + e + ); + } + } + + private List fetchSingleChangesetPage() throws IOException { + LogCommandRequest request = new LogCommandRequest(); + request.setPagingStart(currentNumber); + request.setPagingLimit(1); + List changesets = logCommand.getChangesets(request).getChangesets(); + currentNumber = currentNumber + 1; + return changesets; + } + } + } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java index 6fc214eec8..0495c1514e 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnUnbundleCommandTest.java @@ -24,77 +24,101 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - +import com.google.common.collect.ImmutableList; import com.google.common.io.Files; +import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Person; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.SvnUtil; +import sonia.scm.repository.api.HookChangesetBuilder; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.UnbundleResponse; import java.io.File; import java.io.IOException; +import java.util.List; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -//~--- JDK imports ------------------------------------------------------------ +public class SvnUnbundleCommandTest extends AbstractSvnCommandTestBase { -/** - * - * @author Sebastian Sdorra - */ -public class SvnUnbundleCommandTest extends AbstractSvnCommandTestBase -{ + private final Repository repository = RepositoryTestData.createHeartOfGold("svn"); + private HookContextFactory hookContextFactory; + private ScmEventBus eventBus; + private SvnLogCommand logCommand; + private HookChangesetBuilder hookChangesetBuilder; + + @Captor + private final ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(PostReceiveRepositoryHookEvent.class); + + @Before + public void initMocks() { + hookContextFactory = mock(HookContextFactory.class); + eventBus = mock(ScmEventBus.class); + logCommand = mock(SvnLogCommand.class); + HookContext hookContext = mock(HookContext.class); + hookChangesetBuilder = mock(HookChangesetBuilder.class); + when(hookContextFactory.createContext(any(), eq(repository))).thenReturn(hookContext); + when(hookContext.getChangesetProvider()).thenReturn(hookChangesetBuilder); + } @Test - public void testUnbundle() - throws IOException, SVNException - { + public void shouldFirePostCommitEventAfterUnbundle() throws IOException, SVNException { + Changeset first = new Changeset("1", 0L, new Person("trillian"), "first commit"); + when(hookChangesetBuilder.getChangesetList()).thenReturn(ImmutableList.of(first)); + File bundle = bundle(); SvnContext ctx = createEmptyContext(); //J- - UnbundleResponse res = new SvnUnbundleCommand(ctx) + UnbundleResponse res = new SvnUnbundleCommand(ctx, hookContextFactory, eventBus, logCommand) .unbundle(new UnbundleCommandRequest(Files.asByteSource(bundle)) - ); + ); //J+ - assertThat(res, notNullValue()); - assertThat(res.getChangesetCount(), is(5l)); - + assertThat(res).isNotNull(); + assertThat(res.getChangesetCount()).isEqualTo(5); SVNRepository repo = ctx.open(); + assertThat(repo.getLatestRevision()).isEqualTo(5); + + verify(eventBus).post(eventCaptor.capture()); + PostReceiveRepositoryHookEvent event = eventCaptor.getValue(); + List changesets = event.getContext().getChangesetProvider().getChangesetList(); + assertThat(changesets).hasSize(1); + assertThat(changesets).contains(first); - assertThat(repo.getLatestRevision(), is(5l)); SvnUtil.closeSession(repo); } - private File bundle() throws IOException - { + private File bundle() throws IOException { File file = tempFolder.newFile(); //J- new SvnBundleCommand(createContext()) .bundle(new BundleCommandRequest(Files.asByteSink(file)) - ); + ); //J+ return file; } - /** - * Method description - * - * - * @return - * - * @throws IOException - * @throws SVNException - */ - private SvnContext createEmptyContext() throws IOException, SVNException - { + private SvnContext createEmptyContext() throws IOException, SVNException { File folder = tempFolder.newFolder(); SVNRepositoryFactory.createLocalRepository(folder, true, true); diff --git a/scm-plugins/scm-svn-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/scm-plugins/scm-svn-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline