From b64d34afa481ad610790d85cf74ac89633bb3961 Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Thu, 26 Nov 2020 11:15:26 +0100 Subject: [PATCH] add hook events to tag command and update unit tests --- .../spi/GitRepositoryServiceProvider.java | 10 +- .../spi/GitRepositoryServiceResolver.java | 10 +- .../scm/repository/spi/GitTagCommand.java | 183 +++++++++++------- .../spi/GitRepositoryServiceProviderTest.java | 10 +- .../scm/repository/spi/GitTagCommandTest.java | 61 +++++- 5 files changed, 193 insertions(+), 81 deletions(-) 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 a6f2145611..5b936ba23a 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 @@ -27,8 +27,10 @@ package sonia.scm.repository.spi; import com.google.common.collect.ImmutableSet; import com.google.inject.AbstractModule; import com.google.inject.Injector; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Feature; import sonia.scm.repository.api.Command; +import sonia.scm.repository.api.HookContextFactory; import sonia.scm.security.GPG; import java.util.EnumSet; @@ -63,13 +65,17 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider private final GitContext context; private final GPG gpg; + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; private final Injector commandInjector; //~--- constructors --------------------------------------------------------- - GitRepositoryServiceProvider(Injector injector, GitContext context, GPG gpg) { + GitRepositoryServiceProvider(Injector injector, GitContext context, GPG gpg, HookContextFactory hookContextFactory, ScmEventBus eventBus) { this.context = context; this.gpg = gpg; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; commandInjector = injector.createChildInjector(new AbstractModule() { @Override protected void configure() { @@ -150,7 +156,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider @Override public TagCommand getTagCommand() { - return new GitTagCommand(context, gpg); + return new GitTagCommand(context, gpg, hookContextFactory, eventBus); } @Override 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 1eb9b55e9b..8e65258ada 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 @@ -28,9 +28,11 @@ package sonia.scm.repository.spi; import com.google.inject.Inject; import com.google.inject.Injector; +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.security.GPG; /** @@ -43,18 +45,22 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver { private final Injector injector; private final GitContextFactory contextFactory; private final GPG gpg; + private final HookContextFactory hookContextFactory; + private final ScmEventBus scmEventBus; @Inject - public GitRepositoryServiceResolver(Injector injector, GitContextFactory contextFactory, GPG gpg) { + public GitRepositoryServiceResolver(Injector injector, GitContextFactory contextFactory, GPG gpg, HookContextFactory hookContextFactory) { this.injector = injector; this.contextFactory = contextFactory; this.gpg = gpg; + this.hookContextFactory = hookContextFactory; + this.scmEventBus = ScmEventBus.getInstance(); } @Override public GitRepositoryServiceProvider resolve(Repository repository) { if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { - return new GitRepositoryServiceProvider(injector, contextFactory.create(repository), gpg); + return new GitRepositoryServiceProvider(injector, contextFactory.create(repository), gpg, hookContextFactory, scmEventBus); } return null; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagCommand.java index 31fb9d6249..4d9814f5c4 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitTagCommand.java @@ -30,78 +30,93 @@ import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; -import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.util.RawParseUtils; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.GitUtil; import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.PreReceiveRepositoryHookEvent; +import sonia.scm.repository.RepositoryHookEvent; +import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.Signature; -import sonia.scm.repository.SignatureStatus; import sonia.scm.repository.Tag; -import sonia.scm.repository.api.TagDeleteRequest; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; +import sonia.scm.repository.api.HookFeature; +import sonia.scm.repository.api.HookTagProvider; import sonia.scm.repository.api.TagCreateRequest; +import sonia.scm.repository.api.TagDeleteRequest; import sonia.scm.security.GPG; -import sonia.scm.security.PublicKey; -import java.io.ByteArrayOutputStream; +import javax.inject.Inject; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.Set; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; public class GitTagCommand extends AbstractGitCommand implements TagCommand { private final GPG gpg; + private final HookContextFactory hookContextFactory; + private final ScmEventBus eventBus; - GitTagCommand(GitContext context, GPG gpg) { + @Inject + GitTagCommand(GitContext context, GPG gpg, HookContextFactory hookContextFactory, ScmEventBus eventBus) { super(context); this.gpg = gpg; + this.hookContextFactory = hookContextFactory; + this.eventBus = eventBus; } @Override public Tag create(TagCreateRequest request) { try (Git git = new Git(context.open())) { - Tag tag; String revision = request.getRevision(); - RevObject revObject = null; - Long tagTime = null; + RevObject revObject; + Long tagTime; - if (!Strings.isNullOrEmpty(revision)) { - ObjectId id = git.getRepository().resolve(revision); - - try (RevWalk walk = new RevWalk(git.getRepository())) { - revObject = walk.parseAny(id); - tagTime = GitUtil.getTagTime(walk, id); - } + if (Strings.isNullOrEmpty(revision)) { + throw new IllegalArgumentException("Revision is required"); } - Ref ref; + ObjectId taggedCommitObjectId = git.getRepository().resolve(revision); - if (revObject != null) { - ref = - git.tag() - .setObjectId(revObject) - .setTagger(new PersonIdent("SCM-Manager", "noreply@scm-manager.org")) - .setName(request.getName()) - .call(); - } else { + try (RevWalk walk = new RevWalk(git.getRepository())) { + revObject = walk.parseAny(taggedCommitObjectId); + tagTime = GitUtil.getTagTime(walk, taggedCommitObjectId); + } + + Tag tag = new Tag(request.getName(), revision, tagTime); + + if (revObject == null) { throw new InternalRepositoryException(repository, "could not create tag because revision does not exist"); } - ObjectId objectId; - if (ref.isPeeled()) { - objectId = ref.getPeeledObjectId(); - } else { - objectId = ref.getObjectId(); - } - tag = new Tag(request.getName(), objectId.toString(), tagTime); + RepositoryHookEvent hookEvent = createTagHookEvent(TagHookContextProvider.createHookEvent(tag)); + eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent)); + + Ref ref = + git.tag() + .setObjectId(revObject) + .setTagger(new PersonIdent("SCM-Manager", "noreply@scm-manager.org")) + .setName(request.getName()) + .call(); try (RevWalk walk = new RevWalk(git.getRepository())) { - revObject = walk.parseTag(objectId); - tag.addSignature(getTagSignature((RevTag) revObject)); + revObject = walk.parseTag(ref.getObjectId()); + final Optional tagSignature = GitUtil.getTagSignature(revObject, gpg); + tagSignature.ifPresent(tag::addSignature); } + eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent)); + return tag; } catch (IOException | GitAPIException ex) { throw new InternalRepositoryException(repository, "could not create tag " + request.getName(), ex); @@ -111,58 +126,76 @@ public class GitTagCommand extends AbstractGitCommand implements TagCommand { @Override public void delete(TagDeleteRequest request) { try (Git git = new Git(context.open())) { - git.tagDelete().setTags(request.getName()).call(); + String name = request.getName(); + final Repository repository = git.getRepository(); + Ref tagRef = findTagRef(git, name); + Tag tag; + + try (RevWalk walk = new RevWalk(repository)) { + final RevCommit commit = GitUtil.getCommit(repository, walk, tagRef); + Long tagTime = GitUtil.getTagTime(walk, commit.toObjectId()); + tag = new Tag(name, commit.name(), tagTime); + } + + RepositoryHookEvent hookEvent = createTagHookEvent(TagHookContextProvider.deleteHookEvent(tag)); + eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent)); + git.tagDelete().setTags(name).call(); + eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent)); } catch (GitAPIException | IOException e) { throw new InternalRepositoryException(repository, "could not delete tag", e); } } - private static final byte[] GPG_HEADER = {'g', 'p', 'g', 's', 'i', 'g'}; + private Ref findTagRef(Git git, String name) throws GitAPIException { + final String tagRef = "refs/tags/" + name; + return git.tagList().call().stream().filter(it -> it.getName().equals(tagRef)).findAny().get(); + } - private Signature getTagSignature(RevTag tag) { - byte[] raw = tag.getFullMessage().getBytes(); + private RepositoryHookEvent createTagHookEvent(TagHookContextProvider hookEvent) { + HookContext context = hookContextFactory.createContext(hookEvent, this.context.getRepository()); + return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE); + } - int start = RawParseUtils.headerStart(GPG_HEADER, raw, 0); - if (start < 0) { - return null; + private static class TagHookContextProvider extends HookContextProvider { + private final List newTags; + private final List deletedTags; + + private TagHookContextProvider(List newTags, List deletedTags) { + this.newTags = newTags; + this.deletedTags = deletedTags; } - int end = RawParseUtils.headerEnd(raw, start); - byte[] signature = Arrays.copyOfRange(raw, start, end); - - String publicKeyId = gpg.findPublicKeyId(signature); - if (Strings.isNullOrEmpty(publicKeyId)) { - // key not found - return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()); + static TagHookContextProvider createHookEvent(Tag newTag) { + return new TagHookContextProvider(singletonList(newTag), emptyList()); } - Optional publicKeyById = gpg.findPublicKey(publicKeyId); - if (!publicKeyById.isPresent()) { - // key not found - return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()); + static TagHookContextProvider deleteHookEvent(Tag deletedTag) { + return new TagHookContextProvider(emptyList(), singletonList(deletedTag)); } - PublicKey publicKey = publicKeyById.get(); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - byte[] headerPrefix = Arrays.copyOfRange(raw, 0, start - GPG_HEADER.length - 1); - baos.write(headerPrefix); - - byte[] headerSuffix = Arrays.copyOfRange(raw, end + 1, raw.length); - baos.write(headerSuffix); - } catch (IOException ex) { - // this will never happen, because we are writing into memory - throw new IllegalStateException("failed to write into memory", ex); + @Override + public Set getSupportedFeatures() { + return singleton(HookFeature.BRANCH_PROVIDER); } - boolean verified = publicKey.verify(baos.toByteArray(), signature); - return new Signature( - publicKeyId, - "gpg", - verified ? SignatureStatus.VERIFIED : SignatureStatus.INVALID, - publicKey.getOwner().orElse(null), - publicKey.getContacts() - ); + @Override + public HookTagProvider getTagProvider() { + return new HookTagProvider() { + @Override + public List getCreatedTags() { + return newTags; + } + + @Override + public List getDeletedTags() { + return deletedTags; + } + }; + } + + @Override + public HookChangesetProvider getChangesetProvider() { + return r -> new HookChangesetResponse(emptyList()); + } } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java index 4878a8f4b8..894965ccda 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryServiceProviderTest.java @@ -31,7 +31,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.event.ScmEventBus; import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.api.HookContextFactory; import sonia.scm.security.GPG; import static org.assertj.core.api.Assertions.assertThat; @@ -49,6 +51,12 @@ class GitRepositoryServiceProviderTest { @Mock private GPG gpg; + @Mock + private HookContextFactory hookContextFactory; + + @Mock + private ScmEventBus eventBus; + @Test void shouldCreatePushCommand() { GitRepositoryServiceProvider provider = createProvider(); @@ -63,7 +71,7 @@ class GitRepositoryServiceProviderTest { } private GitRepositoryServiceProvider createProvider() { - return new GitRepositoryServiceProvider(createParentInjector(), context, gpg); + return new GitRepositoryServiceProvider(createParentInjector(), context, gpg, hookContextFactory, eventBus); } private Injector createParentInjector() { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagCommandTest.java index 7c883cc087..12b02e3473 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitTagCommandTest.java @@ -28,10 +28,17 @@ import org.eclipse.jgit.lib.GpgSigner; import org.junit.Before; 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.GitTestHelper; +import sonia.scm.repository.PostReceiveRepositoryHookEvent; +import sonia.scm.repository.PreReceiveRepositoryHookEvent; import sonia.scm.repository.Tag; +import sonia.scm.repository.api.HookContext; +import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.TagDeleteRequest; import sonia.scm.repository.api.TagCreateRequest; import sonia.scm.security.GPG; @@ -41,6 +48,10 @@ import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +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 GitTagCommandTest extends AbstractGitCommandTestBase { @@ -48,6 +59,12 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase { @Mock private GPG gpg; + @Mock + private HookContextFactory hookContextFactory; + + @Mock + private ScmEventBus eventBus; + @Before public void setSigner() { GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); @@ -60,6 +77,23 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase { assertThat(tag).isNotEmpty(); } + @Test + public void shouldPostCreateEvent() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + doNothing().when(eventBus).post(captor.capture()); + when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext); + + createCommand().create(new TagCreateRequest("592d797cd36432e591416e8b2b98154f4f163411", "newtag")); + + 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().getTagProvider().getCreatedTags().get(0).getName()).isEqualTo("newtag"); + assertThat(event.getContext().getTagProvider().getDeletedTags()).isEmpty(); + } + @Test public void shouldDeleteATag() throws IOException { final GitContext context = createContext(); @@ -72,8 +106,27 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase { assertThat(tag).isEmpty(); } + @Test + public void shouldPostDeleteEvent() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + doNothing().when(eventBus).post(captor.capture()); + when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext); + + createCommand().delete(new TagDeleteRequest("test-tag")); + + 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().getTagProvider().getCreatedTags()).isEmpty(); + final Tag deletedTag = event.getContext().getTagProvider().getDeletedTags().get(0); + assertThat(deletedTag.getName()).isEqualTo("test-tag"); + assertThat(deletedTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1"); + } + private GitTagCommand createCommand() { - return new GitTagCommand(createContext(), gpg); + return new GitTagCommand(createContext(), gpg, hookContextFactory, eventBus); } private List readTags(GitContext context) throws IOException { @@ -84,4 +137,10 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase { List branches = readTags(context); return branches.stream().filter(b -> name.equals(b.getName())).findFirst(); } + + private HookContext createMockedContext(InvocationOnMock invocation) { + HookContext mock = mock(HookContext.class); + when(mock.getTagProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getTagProvider()); + return mock; + } }