add hook events to tag command and update unit tests

This commit is contained in:
Konstantin Schaper
2020-11-26 11:15:26 +01:00
parent e3170543cb
commit b64d34afa4
5 changed files with 193 additions and 81 deletions

View File

@@ -27,8 +27,10 @@ package sonia.scm.repository.spi;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Injector; import com.google.inject.Injector;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Feature; import sonia.scm.repository.Feature;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.security.GPG; import sonia.scm.security.GPG;
import java.util.EnumSet; import java.util.EnumSet;
@@ -63,13 +65,17 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
private final GitContext context; private final GitContext context;
private final GPG gpg; private final GPG gpg;
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
private final Injector commandInjector; private final Injector commandInjector;
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
GitRepositoryServiceProvider(Injector injector, GitContext context, GPG gpg) { GitRepositoryServiceProvider(Injector injector, GitContext context, GPG gpg, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
this.context = context; this.context = context;
this.gpg = gpg; this.gpg = gpg;
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
commandInjector = injector.createChildInjector(new AbstractModule() { commandInjector = injector.createChildInjector(new AbstractModule() {
@Override @Override
protected void configure() { protected void configure() {
@@ -150,7 +156,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
@Override @Override
public TagCommand getTagCommand() { public TagCommand getTagCommand() {
return new GitTagCommand(context, gpg); return new GitTagCommand(context, gpg, hookContextFactory, eventBus);
} }
@Override @Override

View File

@@ -28,9 +28,11 @@ package sonia.scm.repository.spi;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Injector; import com.google.inject.Injector;
import sonia.scm.event.ScmEventBus;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.security.GPG; import sonia.scm.security.GPG;
/** /**
@@ -43,18 +45,22 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
private final Injector injector; private final Injector injector;
private final GitContextFactory contextFactory; private final GitContextFactory contextFactory;
private final GPG gpg; private final GPG gpg;
private final HookContextFactory hookContextFactory;
private final ScmEventBus scmEventBus;
@Inject @Inject
public GitRepositoryServiceResolver(Injector injector, GitContextFactory contextFactory, GPG gpg) { public GitRepositoryServiceResolver(Injector injector, GitContextFactory contextFactory, GPG gpg, HookContextFactory hookContextFactory) {
this.injector = injector; this.injector = injector;
this.contextFactory = contextFactory; this.contextFactory = contextFactory;
this.gpg = gpg; this.gpg = gpg;
this.hookContextFactory = hookContextFactory;
this.scmEventBus = ScmEventBus.getInstance();
} }
@Override @Override
public GitRepositoryServiceProvider resolve(Repository repository) { public GitRepositoryServiceProvider resolve(Repository repository) {
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { 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; return null;
} }

View File

@@ -30,78 +30,93 @@ import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref; 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.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk; 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.GitUtil;
import sonia.scm.repository.InternalRepositoryException; 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.Signature;
import sonia.scm.repository.SignatureStatus;
import sonia.scm.repository.Tag; 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.TagCreateRequest;
import sonia.scm.repository.api.TagDeleteRequest;
import sonia.scm.security.GPG; import sonia.scm.security.GPG;
import sonia.scm.security.PublicKey;
import java.io.ByteArrayOutputStream; import javax.inject.Inject;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.List;
import java.util.Collections;
import java.util.Optional; 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 { public class GitTagCommand extends AbstractGitCommand implements TagCommand {
private final GPG gpg; 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); super(context);
this.gpg = gpg; this.gpg = gpg;
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
} }
@Override @Override
public Tag create(TagCreateRequest request) { public Tag create(TagCreateRequest request) {
try (Git git = new Git(context.open())) { try (Git git = new Git(context.open())) {
Tag tag;
String revision = request.getRevision(); String revision = request.getRevision();
RevObject revObject = null; RevObject revObject;
Long tagTime = null; Long tagTime;
if (!Strings.isNullOrEmpty(revision)) { if (Strings.isNullOrEmpty(revision)) {
ObjectId id = git.getRepository().resolve(revision); throw new IllegalArgumentException("Revision is required");
}
ObjectId taggedCommitObjectId = git.getRepository().resolve(revision);
try (RevWalk walk = new RevWalk(git.getRepository())) { try (RevWalk walk = new RevWalk(git.getRepository())) {
revObject = walk.parseAny(id); revObject = walk.parseAny(taggedCommitObjectId);
tagTime = GitUtil.getTagTime(walk, id); tagTime = GitUtil.getTagTime(walk, taggedCommitObjectId);
}
} }
Ref ref; Tag tag = new Tag(request.getName(), revision, tagTime);
if (revObject != null) { if (revObject == null) {
ref = throw new InternalRepositoryException(repository, "could not create tag because revision does not exist");
}
RepositoryHookEvent hookEvent = createTagHookEvent(TagHookContextProvider.createHookEvent(tag));
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
Ref ref =
git.tag() git.tag()
.setObjectId(revObject) .setObjectId(revObject)
.setTagger(new PersonIdent("SCM-Manager", "noreply@scm-manager.org")) .setTagger(new PersonIdent("SCM-Manager", "noreply@scm-manager.org"))
.setName(request.getName()) .setName(request.getName())
.call(); .call();
} else {
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);
try (RevWalk walk = new RevWalk(git.getRepository())) { try (RevWalk walk = new RevWalk(git.getRepository())) {
revObject = walk.parseTag(objectId); revObject = walk.parseTag(ref.getObjectId());
tag.addSignature(getTagSignature((RevTag) revObject)); final Optional<Signature> tagSignature = GitUtil.getTagSignature(revObject, gpg);
tagSignature.ifPresent(tag::addSignature);
} }
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
return tag; return tag;
} catch (IOException | GitAPIException ex) { } catch (IOException | GitAPIException ex) {
throw new InternalRepositoryException(repository, "could not create tag " + request.getName(), ex); throw new InternalRepositoryException(repository, "could not create tag " + request.getName(), ex);
@@ -111,58 +126,76 @@ public class GitTagCommand extends AbstractGitCommand implements TagCommand {
@Override @Override
public void delete(TagDeleteRequest request) { public void delete(TagDeleteRequest request) {
try (Git git = new Git(context.open())) { 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) { } catch (GitAPIException | IOException e) {
throw new InternalRepositoryException(repository, "could not delete tag", 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;
private Signature getTagSignature(RevTag tag) { return git.tagList().call().stream().filter(it -> it.getName().equals(tagRef)).findAny().get();
byte[] raw = tag.getFullMessage().getBytes();
int start = RawParseUtils.headerStart(GPG_HEADER, raw, 0);
if (start < 0) {
return null;
} }
int end = RawParseUtils.headerEnd(raw, start); private RepositoryHookEvent createTagHookEvent(TagHookContextProvider hookEvent) {
byte[] signature = Arrays.copyOfRange(raw, start, end); HookContext context = hookContextFactory.createContext(hookEvent, this.context.getRepository());
return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE);
String publicKeyId = gpg.findPublicKeyId(signature);
if (Strings.isNullOrEmpty(publicKeyId)) {
// key not found
return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet());
} }
Optional<PublicKey> publicKeyById = gpg.findPublicKey(publicKeyId); private static class TagHookContextProvider extends HookContextProvider {
if (!publicKeyById.isPresent()) { private final List<Tag> newTags;
// key not found private final List<Tag> deletedTags;
return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet());
private TagHookContextProvider(List<Tag> newTags, List<Tag> deletedTags) {
this.newTags = newTags;
this.deletedTags = deletedTags;
} }
PublicKey publicKey = publicKeyById.get(); static TagHookContextProvider createHookEvent(Tag newTag) {
return new TagHookContextProvider(singletonList(newTag), emptyList());
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);
} }
boolean verified = publicKey.verify(baos.toByteArray(), signature); static TagHookContextProvider deleteHookEvent(Tag deletedTag) {
return new Signature( return new TagHookContextProvider(emptyList(), singletonList(deletedTag));
publicKeyId, }
"gpg",
verified ? SignatureStatus.VERIFIED : SignatureStatus.INVALID, @Override
publicKey.getOwner().orElse(null), public Set<HookFeature> getSupportedFeatures() {
publicKey.getContacts() return singleton(HookFeature.BRANCH_PROVIDER);
); }
@Override
public HookTagProvider getTagProvider() {
return new HookTagProvider() {
@Override
public List<Tag> getCreatedTags() {
return newTags;
}
@Override
public List<Tag> getDeletedTags() {
return deletedTags;
}
};
}
@Override
public HookChangesetProvider getChangesetProvider() {
return r -> new HookChangesetResponse(emptyList());
}
} }
} }

View File

@@ -31,7 +31,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.security.GPG; import sonia.scm.security.GPG;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -49,6 +51,12 @@ class GitRepositoryServiceProviderTest {
@Mock @Mock
private GPG gpg; private GPG gpg;
@Mock
private HookContextFactory hookContextFactory;
@Mock
private ScmEventBus eventBus;
@Test @Test
void shouldCreatePushCommand() { void shouldCreatePushCommand() {
GitRepositoryServiceProvider provider = createProvider(); GitRepositoryServiceProvider provider = createProvider();
@@ -63,7 +71,7 @@ class GitRepositoryServiceProviderTest {
} }
private GitRepositoryServiceProvider createProvider() { private GitRepositoryServiceProvider createProvider() {
return new GitRepositoryServiceProvider(createParentInjector(), context, gpg); return new GitRepositoryServiceProvider(createParentInjector(), context, gpg, hookContextFactory, eventBus);
} }
private Injector createParentInjector() { private Injector createParentInjector() {

View File

@@ -28,10 +28,17 @@ import org.eclipse.jgit.lib.GpgSigner;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.Tag; 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.TagDeleteRequest;
import sonia.scm.repository.api.TagCreateRequest; import sonia.scm.repository.api.TagCreateRequest;
import sonia.scm.security.GPG; import sonia.scm.security.GPG;
@@ -41,6 +48,10 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat; 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) @RunWith(MockitoJUnitRunner.class)
public class GitTagCommandTest extends AbstractGitCommandTestBase { public class GitTagCommandTest extends AbstractGitCommandTestBase {
@@ -48,6 +59,12 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
@Mock @Mock
private GPG gpg; private GPG gpg;
@Mock
private HookContextFactory hookContextFactory;
@Mock
private ScmEventBus eventBus;
@Before @Before
public void setSigner() { public void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner());
@@ -60,6 +77,23 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
assertThat(tag).isNotEmpty(); assertThat(tag).isNotEmpty();
} }
@Test
public void shouldPostCreateEvent() {
ArgumentCaptor<Object> 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<Object> 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 @Test
public void shouldDeleteATag() throws IOException { public void shouldDeleteATag() throws IOException {
final GitContext context = createContext(); final GitContext context = createContext();
@@ -72,8 +106,27 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
assertThat(tag).isEmpty(); assertThat(tag).isEmpty();
} }
@Test
public void shouldPostDeleteEvent() {
ArgumentCaptor<Object> 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<Object> 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() { private GitTagCommand createCommand() {
return new GitTagCommand(createContext(), gpg); return new GitTagCommand(createContext(), gpg, hookContextFactory, eventBus);
} }
private List<Tag> readTags(GitContext context) throws IOException { private List<Tag> readTags(GitContext context) throws IOException {
@@ -84,4 +137,10 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
List<Tag> branches = readTags(context); List<Tag> branches = readTags(context);
return branches.stream().filter(b -> name.equals(b.getName())).findFirst(); 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;
}
} }