Post receive hook after import (#1544)

Fire post receive repository hook event after pull from remote
and after unbundle (Git, HG and SVN)

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-02-22 09:20:15 +01:00
committed by GitHub
parent eef74e3a50
commit 83a9c90130
31 changed files with 1400 additions and 607 deletions

View File

@@ -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<Changeset> {
private final Iterable<RevCommit> commitIterable;
private final GitChangesetConverter converter;
GitConvertingChangesetIterable(Iterable<RevCommit> commitIterable,
GitChangesetConverter converter) {
this.commitIterable = commitIterable;
this.converter = converter;
}
@Override
public Iterator<Changeset> iterator() {
return new ConvertingChangesetIterator(commitIterable.iterator());
}
class ConvertingChangesetIterator implements Iterator<Changeset> {
private final Iterator<RevCommit> commitIterator;
private ConvertingChangesetIterator(Iterator<RevCommit> commitIterator) {
this.commitIterator = commitIterator;
}
@Override
public boolean hasNext() {
return commitIterator.hasNext();
}
@Override
public Changeset next() {
return converter.createChangeset(commitIterator.next());
}
}
}

View File

@@ -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<Tag> newTags;
private final GitLazyChangesetResolver changesetResolver;
private final List<String> newBranches;
GitImportHookContextProvider(GitChangesetConverter converter,
List<String> newBranches,
List<Tag> newTags,
GitLazyChangesetResolver changesetResolver) {
this.converter = converter;
this.newTags = newTags;
this.changesetResolver = changesetResolver;
this.newBranches = newBranches;
}
@Override
public Set<HookFeature> getSupportedFeatures() {
return ImmutableSet.of(HookFeature.CHANGESET_PROVIDER, HookFeature.BRANCH_PROVIDER, HookFeature.TAG_PROVIDER);
}
@Override
public HookTagProvider getTagProvider() {
return new HookTagProvider() {
@Override
public List<Tag> getCreatedTags() {
return newTags;
}
@Override
public List<Tag> getDeletedTags() {
return Collections.emptyList();
}
};
}
@Override
public HookBranchProvider getBranchProvider() {
return new HookBranchProvider() {
@Override
public List<String> getCreatedOrModified() {
return newBranches;
}
@Override
public List<String> getDeletedOrClosed() {
return Collections.emptyList();
}
};
}
@Override
public HookChangesetProvider getChangesetProvider() {
GitConvertingChangesetIterable changesets = new GitConvertingChangesetIterable(changesetResolver.call(), converter);
return r -> new HookChangesetResponse(changesets);
}
}

View File

@@ -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<Iterable<RevCommit>> {
private final Repository repository;
private final Git git;
public GitLazyChangesetResolver(Repository repository, Git git) {
this.repository = repository;
this.git = git;
}
@Override
public Iterable<RevCommit> 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
);
}
}
}

View File

@@ -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<String> branches,
List<Tag> 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);
}
}

View File

@@ -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<String> branches = getBranchesFromFetchResult(result);
List<Tag> 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<Tag> 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<String> 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());
}
}

View File

@@ -168,7 +168,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
@Override
public UnbundleCommand getUnbundleCommand() {
return new GitUnbundleCommand(context);
return commandInjector.getInstance(GitUnbundleCommand.class);
}
@Override

View File

@@ -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<String> branches = extractBranches(git);
List<Tag> 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<Tag> extractTags(Git git) throws GitAPIException {
return git.tagList().call().stream()
.map(r -> new Tag(r.getName(), r.getObjectId().getName()))
.collect(Collectors.toList());
}
private List<String> 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();
}