diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java index ff3732c836..7a3b260c6f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchCommand.java @@ -35,43 +35,50 @@ package sonia.scm.repository.spi; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.transport.PushResult; -import org.eclipse.jgit.transport.RemoteRefUpdate; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Branch; import sonia.scm.repository.GitUtil; -import sonia.scm.repository.GitWorkdirFactory; import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.PreReceiveRepositoryHookEvent; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.api.BranchRequest; -import sonia.scm.repository.util.WorkingCopy; +import sonia.scm.repository.api.HookBranchProvider; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.repository.api.HookFeature; import java.io.IOException; -import java.util.stream.StreamSupport; +import java.util.List; +import java.util.Set; +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; import static sonia.scm.ContextEntry.ContextBuilder.entity; public class GitBranchCommand extends AbstractGitCommand implements BranchCommand { - private final GitWorkdirFactory workdirFactory; + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; - GitBranchCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) { + GitBranchCommand(GitContext context, Repository repository, HookContextFactory hookContextFactory, ScmEventBus eventBus) { super(context, repository); - this.workdirFactory = workdirFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; } @Override public Branch branch(BranchRequest request) { - try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context, request.getParentBranch())) { - Git clone = new Git(workingCopy.getWorkingRepository()); - Ref ref = clone.branchCreate().setName(request.getNewBranch()).call(); - Iterable call = clone.push().add(request.getNewBranch()).call(); - StreamSupport.stream(call.spliterator(), false) - .flatMap(pushResult -> pushResult.getRemoteUpdates().stream()) - .filter(remoteRefUpdate -> remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK) - .findFirst() - .ifPresent(r -> this.handlePushError(r, request, context.getRepository())); + try (Git git = new Git(context.open())) { + RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.createHookEvent(request.getNewBranch())); + eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent)); + Ref ref = git.branchCreate().setStartPoint(request.getParentBranch()).setName(request.getNewBranch()).call(); + eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent)); return Branch.normalBranch(request.getNewBranch(), GitUtil.getId(ref.getObjectId())); - } catch (GitAPIException ex) { + } catch (GitAPIException | IOException ex) { throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex); } } @@ -79,21 +86,64 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman @Override public void delete(String branchName) { try (Git gitRepo = new Git(context.open())) { + RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.deleteHookEvent(branchName)); + eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent)); gitRepo .branchDelete() .setBranchNames(branchName) .setForce(true) .call(); + eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent)); } catch (GitAPIException | IOException ex) { throw new InternalRepositoryException(entity(context.getRepository()), String.format("Could not delete branch: %s", branchName)); } } - private void handlePushError(RemoteRefUpdate remoteRefUpdate, BranchRequest request, Repository repository) { - if (remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK) { - // TODO handle failed remote update - throw new IntegrateChangesFromWorkdirException(repository, - String.format("Could not push new branch '%s' into central repository", request.getNewBranch())); + private RepositoryHookEvent createBranchHookEvent(BranchHookContextProvider hookEvent) { + HookContext context = hookContextFactory.createContext(hookEvent, this.context.getRepository()); + return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE); + } + + private static class BranchHookContextProvider extends HookContextProvider { + private final List newBranches; + private final List deletedBranches; + + private BranchHookContextProvider(List newBranches, List deletedBranches) { + this.newBranches = newBranches; + this.deletedBranches = deletedBranches; + } + + static BranchHookContextProvider createHookEvent(String newBranch) { + return new BranchHookContextProvider(singletonList(newBranch), emptyList()); + } + + static BranchHookContextProvider deleteHookEvent(String deletedBranch) { + return new BranchHookContextProvider(emptyList(), singletonList(deletedBranch)); + } + + @Override + public Set getSupportedFeatures() { + return singleton(HookFeature.BRANCH_PROVIDER); + } + + @Override + public HookBranchProvider getBranchProvider() { + return new HookBranchProvider() { + @Override + public List getCreatedOrModified() { + return newBranches; + } + + @Override + public List getDeletedOrClosed() { + return deletedBranches; + } + }; + } + + @Override + public HookChangesetProvider getChangesetProvider() { + return r -> new HookChangesetResponse(emptyList()); } } } 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 800f011597..e1ae58ada5 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 @@ -35,10 +35,12 @@ package sonia.scm.repository.spi; import com.google.common.collect.ImmutableSet; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Feature; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; +import sonia.scm.repository.api.HookContextFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.IOException; @@ -78,10 +80,12 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- - public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) { + public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { this.handler = handler; this.repository = repository; this.lfsBlobStoreFactory = lfsBlobStoreFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider); } @@ -134,7 +138,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public BranchCommand getBranchCommand() { - return new GitBranchCommand(context, repository, handler.getWorkdirFactory()); + return new GitBranchCommand(context, repository, hookContextFactory, eventBus); } /** @@ -293,4 +297,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider private final Repository repository; private final LfsBlobStoreFactory lfsBlobStoreFactory; + + private final HookContextFactory hookContextFactory; + + private final ScmEventBus eventBus; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java index 547c6b25f8..7fc5fb27c4 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java @@ -36,9 +36,11 @@ package sonia.scm.repository.spi; import com.google.inject.Inject; import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider; +import sonia.scm.event.ScmEventBus; import sonia.scm.plugin.Extension; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; +import sonia.scm.repository.api.HookContextFactory; import sonia.scm.web.lfs.LfsBlobStoreFactory; /** @@ -51,12 +53,16 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { private final GitRepositoryHandler handler; private final GitRepositoryConfigStoreProvider storeProvider; private final LfsBlobStoreFactory lfsBlobStoreFactory; + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; @Inject - public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) { + public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) { this.handler = handler; this.storeProvider = storeProvider; this.lfsBlobStoreFactory = lfsBlobStoreFactory; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; } @Override @@ -64,7 +70,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { GitRepositoryServiceProvider provider = null; if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory); + provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus); } return provider; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java index 65745228b7..408b2cca0c 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchCommandTest.java @@ -1,23 +1,38 @@ package sonia.scm.repository.spi; -import org.assertj.core.api.Assertions; -import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Branch; import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.PreReceiveRepositoryHookEvent; import sonia.scm.repository.api.BranchRequest; -import sonia.scm.repository.util.WorkdirProvider; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; import java.io.IOException; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) public class GitBranchCommandTest extends AbstractGitCommandTestBase { - @Rule - public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + @Mock + private HookContextFactory hookContextFactory; + @Mock + private ScmEventBus eventBus; @Test public void shouldCreateBranchWithDefinedSourceBranch() throws IOException { @@ -29,10 +44,10 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase { branchRequest.setParentBranch(source.getName()); branchRequest.setNewBranch("new_branch"); - new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest); + createCommand().branch(branchRequest); Branch newBranch = findBranch(context, "new_branch"); - Assertions.assertThat(newBranch.getRevision()).isEqualTo(source.getRevision()); + assertThat(newBranch.getRevision()).isEqualTo(source.getRevision()); } private Branch findBranch(GitContext context, String name) throws IOException { @@ -44,32 +59,79 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase { public void shouldCreateBranch() throws IOException { GitContext context = createContext(); - Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isEmpty(); + assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isEmpty(); BranchRequest branchRequest = new BranchRequest(); branchRequest.setNewBranch("new_branch"); - new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest); + createCommand().branch(branchRequest); - Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); + assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty(); } @Test public void shouldDeleteBranch() throws IOException { GitContext context = createContext(); String branchToBeDeleted = "squash"; - new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).delete(branchToBeDeleted); - Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals(branchToBeDeleted)).isEmpty(); + createCommand().delete(branchToBeDeleted); + assertThat(readBranches(context)).filteredOn(b -> b.getName().equals(branchToBeDeleted)).isEmpty(); } @Test public void shouldThrowInternalRepositoryException() { - GitContext context = createContext(); String branchToBeDeleted = "master"; - assertThrows(InternalRepositoryException.class, () -> new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).delete(branchToBeDeleted)); + assertThrows(InternalRepositoryException.class, () -> createCommand().delete(branchToBeDeleted)); + } + + private GitBranchCommand createCommand() { + return new GitBranchCommand(createContext(), repository, hookContextFactory, eventBus); } private List readBranches(GitContext context) throws IOException { return new GitBranchesCommand(context, repository).getBranches(); } + + @Test + public void shouldPostCreateEvents() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + doNothing().when(eventBus).post(captor.capture()); + when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext); + + BranchRequest branchRequest = new BranchRequest(); + branchRequest.setParentBranch("mergeable"); + branchRequest.setNewBranch("new_branch"); + + createCommand().branch(branchRequest); + + List events = captor.getAllValues(); + assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class); + assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class); + + PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0); + assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).containsExactly("new_branch"); + assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).isEmpty(); + } + + @Test + public void shouldPostDeleteEvents() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + doNothing().when(eventBus).post(captor.capture()); + when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext); + + createCommand().delete("squash"); + + List events = captor.getAllValues(); + assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class); + assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class); + + PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0); + assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).containsExactly("squash"); + assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).isEmpty(); + } + + private HookContext createMockedContext(InvocationOnMock invocation) { + HookContext mock = mock(HookContext.class); + when(mock.getBranchProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getBranchProvider()); + return mock; + } }