Implement commit search features for git (#2111)

Implements the required features for the commit search plugin for git.
This commit is contained in:
René Pfeuffer
2022-09-01 15:31:33 +02:00
committed by GitHub
parent d7bf14518a
commit e0a56a1369
15 changed files with 925 additions and 356 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Implement commit search features for git ([#2111](https://github.com/scm-manager/scm-manager/pull/2111))

View File

@@ -26,13 +26,10 @@ package sonia.scm.repository.api;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.io.DeepCopy;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.PreProcessorUtil;
@@ -41,13 +38,8 @@ import sonia.scm.repository.spi.HookChangesetProvider;
import sonia.scm.repository.spi.HookChangesetRequest;
import sonia.scm.repository.spi.HookChangesetResponse;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* The {@link HookChangesetBuilder} is able to return all {@link Changeset}s
@@ -113,49 +105,21 @@ public final class HookChangesetBuilder
{
HookChangesetResponse hookChangesetResponse = provider.handleRequest(request);
Iterable<Changeset> changesets = hookChangesetResponse.getChangesets();
if (!disablePreProcessors)
{
changesets = Iterables.transform(changesets,
new Function<Changeset, Changeset>()
{
@Override
public Changeset apply(Changeset c)
{
Changeset copy = null;
try
{
copy = DeepCopy.copy(c);
preProcessorUtil.prepareForReturn(repository, copy);
}
catch (IOException ex)
{
logger.error("could not create a copy of changeset", ex);
}
if (copy == null)
{
copy = c;
}
return copy;
}
});
}
return changesets;
return applyPreprocessorsIfNotDisabled(changesets);
}
public Iterable<Changeset> getRemovedChangesets() {
HookChangesetResponse hookChangesetResponse = provider.handleRequest(request);
Iterable<Changeset> changesets = hookChangesetResponse.getRemovedChangesets();
return applyPreprocessorsIfNotDisabled(changesets);
}
if (!disablePreProcessors)
{
changesets = StreamSupport.stream(changesets.spliterator(), false).map(c -> {
private Iterable<Changeset> applyPreprocessorsIfNotDisabled(Iterable<Changeset> changesets) {
if (disablePreProcessors) {
return changesets;
}
return Iterables.transform(changesets, c -> {
Changeset copy = null;
try {
@@ -166,14 +130,11 @@ public final class HookChangesetBuilder
}
if (copy == null) {
return c;
copy = c;
}
return copy;
}).collect(Collectors.toList());
}
return changesets;
});
}
//~--- set methods ----------------------------------------------------------

View File

@@ -65,12 +65,8 @@ public class GitChangesetConverter implements Closeable {
this.treeWalk = new TreeWalk(repository);
}
public Changeset createChangeset(RevCommit commit) {
return createChangeset(commit, Collections.emptyList());
}
public Changeset createChangeset(RevCommit commit, String branch) {
return createChangeset(commit, Lists.newArrayList(branch));
public Changeset createChangeset(RevCommit commit, String... branches) {
return createChangeset(commit, Arrays.asList(branches));
}
public Changeset createChangeset(RevCommit commit, List<String> branches) {
@@ -112,7 +108,7 @@ public class GitChangesetConverter implements Closeable {
changeset.getTags().addAll(Lists.newArrayList(tagCollection));
}
changeset.setBranches(branches);
changeset.setBranches(new ArrayList<>(branches));
Signature signature = createSignature(commit);
if (signature != null) {
@@ -142,7 +138,7 @@ public class GitChangesetConverter implements Closeable {
}
Optional<PublicKey> publicKeyById = gpg.findPublicKey(publicKeyId);
if (!publicKeyById.isPresent()) {
if (publicKeyById.isEmpty()) {
// key not found
return new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet());
}

View File

@@ -1,217 +0,0 @@
/*
* 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;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.web.CollectingPackParserListener;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
*
* @author Sebastian Sdorra
*/
public class GitHookChangesetCollector
{
/**
* the logger for GitHookChangesetCollector
*/
private static final Logger logger =
LoggerFactory.getLogger(GitHookChangesetCollector.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs a new instance
*
*
* @param rpack
* @param receiveCommands
*/
public GitHookChangesetCollector(GitChangesetConverterFactory converterFactory, ReceivePack rpack,
List<ReceiveCommand> receiveCommands)
{
this.converterFactory = converterFactory;
this.rpack = rpack;
this.receiveCommands = receiveCommands;
this.listener = CollectingPackParserListener.get(rpack);
}
//~--- methods --------------------------------------------------------------
/**
* Collect all new changesets from the received hook.
*
* @return new changesets
*/
public List<Changeset> collectChangesets()
{
Map<String, Changeset> changesets = Maps.newLinkedHashMap();
try (
org.eclipse.jgit.lib.Repository repository = rpack.getRepository();
RevWalk walk = rpack.getRevWalk();
GitChangesetConverter converter = converterFactory.create(repository, walk)
) {
repository.incrementOpen();
for (ReceiveCommand rc : receiveCommands)
{
String ref = rc.getRefName();
logger.trace("handle receive command, type={}, ref={}, result={}", rc.getType(), ref, rc.getResult());
if (rc.getType() == ReceiveCommand.Type.DELETE)
{
logger.debug("skip delete of ref {}", ref);
}
else if (! GitUtil.isBranch(ref))
{
logger.debug("skip ref {}, because it is not a branch", ref);
}
else
{
try
{
collectChangesets(changesets, converter, walk, rc);
}
catch (IOException ex)
{
StringBuilder builder = new StringBuilder();
builder.append("could not handle receive command, type=");
builder.append(rc.getType()).append(", ref=");
builder.append(rc.getRefName()).append(", result=");
builder.append(rc.getResult());
logger.error(builder.toString(), ex);
}
}
}
}
catch (Exception ex)
{
logger.error("could not collect changesets", ex);
}
return Lists.newArrayList(changesets.values());
}
private void collectChangesets(Map<String, Changeset> changesets,
GitChangesetConverter converter, RevWalk walk, ReceiveCommand rc)
throws IOException
{
ObjectId newId = rc.getNewId();
String branch = GitUtil.getBranch(rc.getRefName());
walk.reset();
walk.sort(RevSort.TOPO);
walk.sort(RevSort.REVERSE, true);
logger.trace("mark {} as start for rev walk", newId.getName());
walk.markStart(walk.parseCommit(newId));
ObjectId oldId = rc.getOldId();
if ((oldId != null) && !oldId.equals(ObjectId.zeroId()))
{
logger.trace("mark {} as uninteresting for rev walk", oldId.getName());
walk.markUninteresting(walk.parseCommit(oldId));
}
RevCommit commit = walk.next();
while (commit != null)
{
String id = commit.getId().name();
Changeset changeset = changesets.get(id);
if (changeset != null)
{
logger.trace(
"commit {} already received durring this push, add branch {} to the commit",
commit, branch);
changeset.getBranches().add(branch);
}
else
{
// only append new commits
if (listener.isNew(commit))
{
// parse commit body to avoid npe
walk.parseBody(commit);
changeset = converter.createChangeset(commit, branch);
logger.trace("retrieve commit {} for hook", changeset.getId());
changesets.put(id, changeset);
}
else
{
logger.trace("commit {} was already received", commit.getId());
}
}
commit = walk.next();
}
}
//~--- fields ---------------------------------------------------------------
/** listener to track new objects */
private final CollectingPackParserListener listener;
private final List<ReceiveCommand> receiveCommands;
private final GitChangesetConverterFactory converterFactory;
private final ReceivePack rpack;
}

View File

@@ -68,8 +68,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static java.util.Optional.empty;
import static java.util.Optional.of;
@@ -233,6 +235,17 @@ public final class GitUtil {
return Strings.nullToEmpty(refName).startsWith(PREFIX_HEADS);
}
/**
* Returns {@code true} if the provided reference name is a tag name.
*
* @param refName reference name
* @return {@code true} if the name is a tag name
* @since 2.39.0
*/
public static boolean isTag(String refName) {
return Strings.nullToEmpty(refName).startsWith(PREFIX_TAG);
}
public static Ref getBranchIdOrCurrentHead(org.eclipse.jgit.lib.Repository gitRepository, String requestedBranch) throws IOException {
if (Strings.isNullOrEmpty(requestedBranch)) {
logger.trace("no default branch configured, use repository head as default");
@@ -700,4 +713,21 @@ public final class GitUtil {
.setRefSpecs(new RefSpec(REF_SPEC))
.setTagOpt(TagOpt.FETCH_TAGS);
}
public static Stream<RevCommit> getAllCommits(org.eclipse.jgit.lib.Repository repository, RevWalk revWalk) throws IOException {
return repository.getRefDatabase()
.getRefs()
.stream()
.map(ref -> getCommitFromRef(ref, revWalk))
.filter(Objects::nonNull);
}
public static RevCommit getCommitFromRef(Ref ref, RevWalk revWalk) {
try {
return getCommit(null, revWalk, ref);
} catch (IOException e) {
logger.info("could not get commit for {}", ref, e);
return null;
}
}
}

View File

@@ -27,9 +27,14 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CannotDeleteCurrentBranchException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
@@ -44,28 +49,41 @@ import sonia.scm.repository.api.HookFeature;
import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static java.util.Collections.*;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.eclipse.jgit.lib.ObjectId.zeroId;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
public class GitBranchCommand extends AbstractGitCommand implements BranchCommand {
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
private final GitChangesetConverterFactory converterFactory;
@Inject
GitBranchCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
GitBranchCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus, GitChangesetConverterFactory converterFactory) {
super(context);
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
this.converterFactory = converterFactory;
}
@Override
public Branch branch(BranchRequest request) {
try (Git git = new Git(context.open())) {
RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.createHookEvent(request.getNewBranch()));
ObjectId newRef;
if (request.getParentBranch() == null) {
newRef = git.log().call().iterator().next();
} else {
newRef = getRef(git.getRepository(), request.getParentBranch());
}
RepositoryHookEvent hookEvent = createBranchHookEvent(createHookEvent(request.getNewBranch(), newRef));
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
Ref ref = git.branchCreate().setStartPoint(request.getParentBranch()).setName(request.getNewBranch()).call();
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
@@ -78,7 +96,8 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
@Override
public void deleteOrClose(String branchName) {
try (Git gitRepo = new Git(context.open())) {
RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.deleteHookEvent(branchName));
ObjectId oldRef = getRef(gitRepo.getRepository(), branchName);
RepositoryHookEvent hookEvent = createBranchHookEvent(deleteHookEvent(branchName, oldRef));
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
gitRepo
.branchDelete()
@@ -98,21 +117,31 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE);
}
private static class BranchHookContextProvider extends HookContextProvider {
private ObjectId getRef(Repository gitRepo, String branch) {
try {
return gitRepo.getRefDatabase().findRef("refs/heads/" + branch).getObjectId();
} catch (IOException e) {
throw new InternalRepositoryException(repository, "error reading ref for branch", e);
}
}
private BranchHookContextProvider createHookEvent(String newBranch, ObjectId objectId) {
return new BranchHookContextProvider(singletonList(newBranch), emptyList(), objectId);
}
private BranchHookContextProvider deleteHookEvent(String deletedBranch, ObjectId oldObjectId) {
return new BranchHookContextProvider(emptyList(), singletonList(deletedBranch), oldObjectId);
}
private class BranchHookContextProvider extends HookContextProvider {
private final List<String> newBranches;
private final List<String> deletedBranches;
private final ObjectId objectId;
private BranchHookContextProvider(List<String> newBranches, List<String> deletedBranches) {
private BranchHookContextProvider(List<String> newBranches, List<String> deletedBranches, ObjectId objectId) {
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));
this.objectId = objectId;
}
@Override
@@ -137,7 +166,31 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
@Override
public HookChangesetProvider getChangesetProvider() {
return r -> new HookChangesetResponse(emptyList());
Repository gitRepo;
try {
gitRepo = context.open();
} catch (IOException e) {
throw new InternalRepositoryException(repository, "failed to open repository for post receive hook after internal change", e);
}
Collection<ReceiveCommand> receiveCommands = new ArrayList<>();
newBranches.stream()
.map(branch -> new ReceiveCommand(zeroId(), objectId, "refs/heads/" + branch))
.forEach(receiveCommands::add);
deletedBranches.stream()
.map(branch -> new ReceiveCommand(objectId, zeroId(), "refs/heads/" + branch))
.forEach(receiveCommands::add);
return x -> {
GitHookChangesetCollector collector =
GitHookChangesetCollector.collectChangesets(
converterFactory,
receiveCommands,
gitRepo,
new RevWalk(gitRepo),
commit -> false // we cannot create new commits with this tag command
);
return new HookChangesetResponse(collector.getAddedChangesets(), collector.getRemovedChangesets());
};
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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 lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Optional;
import java.util.stream.Collectors;
@Slf4j
public class GitChangesetsCommand extends AbstractGitCommand implements ChangesetsCommand {
private final GitChangesetConverterFactory converterFactory;
@Inject
GitChangesetsCommand(GitContext context, GitChangesetConverterFactory converterFactory) {
super(context);
this.converterFactory = converterFactory;
}
@Override
public Iterable<Changeset> getChangesets(ChangesetsCommandRequest request) {
try {
log.debug("computing changesets for repository {}", repository);
Repository gitRepository = open();
try (RevWalk revWalk = new RevWalk(gitRepository)) {
revWalk.markStart(GitUtil.getAllCommits(gitRepository, revWalk).collect(Collectors.toList()));
log.trace("got git iterator for all changesets for repository {}", repository);
Iterator<RevCommit> iterator = revWalk.iterator();
return () -> new ChangesetIterator(iterator, revWalk, gitRepository);
} finally {
log.trace("returned iterator for all changesets for repository {}", gitRepository);
}
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "failed to get latest commit", e);
}
}
@Override
public Optional<Changeset> getLatestChangeset() {
try {
Repository repository = open();
try (RevWalk revWalk = new RevWalk(repository)) {
return GitUtil.getAllCommits(repository, revWalk)
.max(new ByCommitDateComparator())
.map(commit -> converterFactory.create(repository, revWalk).createChangeset(commit));
}
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "failed to get latest commit", e);
}
}
private class ChangesetIterator implements Iterator<Changeset> {
private final Iterator<RevCommit> iterator;
private final GitChangesetConverter changesetConverter;
private final RevWalk revWalk;
ChangesetIterator(Iterator<RevCommit> iterator, RevWalk revWalk, Repository gitRepository) {
this.iterator = iterator;
this.changesetConverter = converterFactory.create(gitRepository, revWalk);
this.revWalk = revWalk;
}
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Changeset next() {
try {
log.trace("mapping changeset for repository {}", repository);
return changesetConverter.createChangeset(revWalk.parseCommit(iterator.next()));
} catch (IOException e) {
throw new InternalRepositoryException(context.getRepository(), "failed to create changeset for single git revision", e);
}
}
}
private static class ByCommitDateComparator implements Comparator<RevCommit> {
@Override
public int compare(RevCommit rev1, RevCommit rev2) {
long commitTime1 = rev1.getCommitTime();
long commitTime2 = rev2.getCommitTime();
return Long.compare(commitTime1, commitTime2);
}
}
}

View File

@@ -0,0 +1,236 @@
/*
* 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.Maps;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.web.CollectingPackParserListener;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import static java.util.Collections.unmodifiableCollection;
/**
* @author Sebastian Sdorra
*/
class GitHookChangesetCollector {
private static final Logger LOG = LoggerFactory.getLogger(GitHookChangesetCollector.class);
private final Collection<ReceiveCommand> receiveCommands;
private final GitChangesetConverterFactory converterFactory;
/**
* listener to track new objects
*/
private final NewCommitDetector newCommitDetector;
private final Repository repository;
private final RevWalk walk;
private final Map<String, Changeset> addedChangesets = Maps.newLinkedHashMap();
private final Map<String, Changeset> removedChangesets = Maps.newLinkedHashMap();
private GitHookChangesetCollector(GitChangesetConverterFactory converterFactory, Collection<ReceiveCommand> receiveCommands, NewCommitDetector newCommitDetector, Repository repository, RevWalk walk) {
this.converterFactory = converterFactory;
this.receiveCommands = receiveCommands;
this.newCommitDetector = newCommitDetector;
this.repository = repository;
this.walk = walk;
}
static GitHookChangesetCollector collectChangesets(GitChangesetConverterFactory converterFactory, Collection<ReceiveCommand> receiveCommands, ReceivePack rpack) {
try (Repository repository = rpack.getRepository();
RevWalk walk = rpack.getRevWalk()) {
CollectingPackParserListener listener = CollectingPackParserListener.get(rpack);
return collectChangesets(converterFactory, receiveCommands, repository, walk, listener::isNew);
}
}
static GitHookChangesetCollector collectChangesets(GitChangesetConverterFactory converterFactory, Collection<ReceiveCommand> receiveCommands, Repository repository, RevWalk walk, NewCommitDetector newCommitDetector) {
GitHookChangesetCollector gitHookChangesetCollector = new GitHookChangesetCollector(converterFactory, receiveCommands, newCommitDetector, repository, walk);
gitHookChangesetCollector.collectChangesets();
return gitHookChangesetCollector;
}
/**
* Collect all new changesets from the received hook. Afterwards, the results can be
* retrieved with {@link #getAddedChangesets()} and {@link #getRemovedChangesets()}
*/
private void collectChangesets() {
try (GitChangesetConverter converter = converterFactory.create(repository, walk)) {
repository.incrementOpen();
for (ReceiveCommand rc : receiveCommands) {
String ref = rc.getRefName();
LOG.trace("handle receive command, type={}, ref={}, result={}", rc.getType(), ref, rc.getResult());
handle(repository, walk, converter, rc, ref);
}
} catch (Exception ex) {
LOG.error("could not collect changesets", ex);
}
}
Iterable<Changeset> getAddedChangesets() {
return unmodifiableCollection(addedChangesets.values());
}
Iterable<Changeset> getRemovedChangesets() {
return unmodifiableCollection(removedChangesets.values());
}
void handle(Repository repository, RevWalk walk, GitChangesetConverter converter, ReceiveCommand rc, String ref) {
try {
if (!(GitUtil.isBranch(ref) || GitUtil.isTag(ref))) {
LOG.debug("skip ref {}, because it is neither branch nor tag", ref);
} else if (rc.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD) {
LOG.debug("handle deleted/added ref {}", ref);
collectRemovedChangeset(repository, walk, converter, rc);
collectAddedChangesets(converter, walk, rc, ref);
} else if (rc.getType() == ReceiveCommand.Type.DELETE) {
LOG.debug("handle deleted ref {}", ref);
collectRemovedChangeset(repository, walk, converter, rc);
} else {
LOG.debug("handle added ref {}", ref);
collectAddedChangesets(converter, walk, rc, ref);
}
} catch (IOException ex) {
String message = "could not handle receive command, type=" +
rc.getType() + ", ref=" +
rc.getRefName() + ", result=" +
rc.getResult();
LOG.error(message, ex);
}
}
private void collectAddedChangesets(GitChangesetConverter converter,
RevWalk walk,
ReceiveCommand rc,
String ref)
throws IOException {
walk.reset();
ObjectId newId = rc.getNewId();
String branch = GitUtil.getBranch(rc.getRefName());
walk.sort(RevSort.TOPO);
walk.sort(RevSort.REVERSE, true);
LOG.trace("mark {} as start for rev walk", newId.getName());
walk.markStart(walk.parseCommit(newId));
ObjectId oldId = rc.getOldId();
if ((oldId != null) && !oldId.equals(ObjectId.zeroId())) {
LOG.trace("mark {} as uninteresting for rev walk", oldId.getName());
walk.markUninteresting(walk.parseCommit(oldId));
}
RevCommit commit = walk.next();
while (commit != null) {
String id = commit.getId().name();
Changeset changeset = addedChangesets.get(id);
if (changeset != null) {
if (GitUtil.isBranch(ref)) {
LOG.trace(
"commit {} already received during this push, add branch {} to the commit",
commit, branch);
changeset.getBranches().add(branch);
}
} else if (newCommitDetector.isNew(commit)) {
// only append new commits
addToCollection(addedChangesets, converter, walk, commit, id, branch);
} else {
LOG.trace("commit {} was already received", commit.getId());
}
commit = walk.next();
}
}
private void collectRemovedChangeset(Repository repository, RevWalk walk, GitChangesetConverter converter, ReceiveCommand rc) throws IOException {
walk.reset();
ObjectId oldId = rc.getOldId();
walk.markStart(walk.parseCommit(oldId));
GitUtil.getAllCommits(repository, walk).forEach(c -> {
try {
walk.markUninteresting(c);
} catch (IOException e) {
throw new IllegalStateException("failed to mark commit as to be ignored", e);
}
});
RevCommit commit = walk.next();
while (commit != null) {
String id = commit.getId().name();
Changeset changeset = removedChangesets.get(id);
if (changeset == null) {
addToCollection(removedChangesets, converter, walk, commit, id);
}
commit = walk.next();
}
}
private void addToCollection(Map<String, Changeset> changesets, GitChangesetConverter converter, RevWalk walk, RevCommit commit, String id, String... branches) throws IOException {
// parse commit body to avoid npe
walk.parseBody(commit);
Changeset newChangeset = converter.createChangeset(commit, branches);
LOG.trace("retrieve commit {} for hook", newChangeset.getId());
changesets.put(id, newChangeset);
}
interface NewCommitDetector {
boolean isNew(RevCommit commit);
}
}

View File

@@ -24,15 +24,9 @@
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitHookChangesetCollector;
//~--- JDK imports ------------------------------------------------------------
import java.util.List;
@@ -58,8 +52,8 @@ public class GitHookChangesetProvider implements HookChangesetProvider {
@Override
public synchronized HookChangesetResponse handleRequest(HookChangesetRequest request) {
if (response == null) {
GitHookChangesetCollector collector = new GitHookChangesetCollector(converterFactory, receivePack, receiveCommands);
response = new HookChangesetResponse(collector.collectChangesets());
GitHookChangesetCollector collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, receivePack);
response = new HookChangesetResponse(collector.getAddedChangesets(), collector.getRemovedChangesets());
}
return response;
}

View File

@@ -59,7 +59,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
Command.UNBUNDLE,
Command.MIRROR,
Command.FILE_LOCK,
Command.BRANCH_DETAILS
Command.BRANCH_DETAILS,
Command.CHANGESETS
);
protected static final Set<Feature> FEATURES = EnumSet.of(
@@ -192,6 +193,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
return commandInjector.getInstance(GitBranchDetailsCommand.class);
}
@Override
public ChangesetsCommand getChangesetsCommand() {
return commandInjector.getInstance(GitChangesetsCommand.class);
}
@Override
public Set<Command> getSupportedCommands() {
return COMMANDS;

View File

@@ -35,7 +35,9 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
@@ -53,6 +55,8 @@ import sonia.scm.user.User;
import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -60,18 +64,23 @@ import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.eclipse.jgit.lib.ObjectId.fromString;
import static org.eclipse.jgit.lib.ObjectId.zeroId;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
public class GitTagCommand extends AbstractGitCommand implements TagCommand {
public static final String REFS_TAGS_PREFIX = "refs/tags/";
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
private final GitChangesetConverterFactory converterFactory;
@Inject
GitTagCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
GitTagCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus, GitChangesetConverterFactory converterFactory) {
super(context);
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
this.converterFactory = converterFactory;
}
@Override
@@ -105,7 +114,7 @@ public class GitTagCommand extends AbstractGitCommand implements TagCommand {
Tag tag = new Tag(name, revision, tagTime);
RepositoryHookEvent hookEvent = createTagHookEvent(TagHookContextProvider.createHookEvent(tag));
RepositoryHookEvent hookEvent = createTagHookEvent(createHookEvent(tag), RepositoryHookType.PRE_RECEIVE);
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
User user = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
@@ -140,7 +149,7 @@ public class GitTagCommand extends AbstractGitCommand implements TagCommand {
// Deleting a non-existent tag is a valid action and simply succeeds without
// anything happening.
if (!tagRef.isPresent()) {
if (tagRef.isEmpty()) {
return;
}
@@ -150,26 +159,37 @@ public class GitTagCommand extends AbstractGitCommand implements TagCommand {
tag = new Tag(name, commit.name(), tagTime);
}
RepositoryHookEvent hookEvent = createTagHookEvent(TagHookContextProvider.deleteHookEvent(tag));
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
eventBus.post(new PreReceiveRepositoryHookEvent(
createTagHookEvent(deleteHookEvent(tag), RepositoryHookType.PRE_RECEIVE)
));
git.tagDelete().setTags(name).call();
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
eventBus.post(new PostReceiveRepositoryHookEvent(
createTagHookEvent(deleteHookEvent(tag), RepositoryHookType.POST_RECEIVE)
));
} catch (GitAPIException | IOException e) {
throw new InternalRepositoryException(repository, "could not delete tag " + name, e);
}
}
private Optional<Ref> findTagRef(Git git, String name) throws GitAPIException {
final String tagRef = "refs/tags/" + name;
final String tagRef = REFS_TAGS_PREFIX + name;
return git.tagList().call().stream().filter(it -> it.getName().equals(tagRef)).findAny();
}
private RepositoryHookEvent createTagHookEvent(TagHookContextProvider hookEvent) {
private RepositoryHookEvent createTagHookEvent(TagHookContextProvider hookEvent, RepositoryHookType type) {
HookContext context = hookContextFactory.createContext(hookEvent, this.context.getRepository());
return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE);
return new RepositoryHookEvent(context, this.context.getRepository(), type);
}
private static class TagHookContextProvider extends HookContextProvider {
private TagHookContextProvider createHookEvent(Tag newTag) {
return new TagHookContextProvider(singletonList(newTag), emptyList());
}
private TagHookContextProvider deleteHookEvent(Tag deletedTag) {
return new TagHookContextProvider(emptyList(), singletonList(deletedTag));
}
private class TagHookContextProvider extends HookContextProvider {
private final List<Tag> newTags;
private final List<Tag> deletedTags;
@@ -178,14 +198,6 @@ public class GitTagCommand extends AbstractGitCommand implements TagCommand {
this.deletedTags = deletedTags;
}
static TagHookContextProvider createHookEvent(Tag newTag) {
return new TagHookContextProvider(singletonList(newTag), emptyList());
}
static TagHookContextProvider deleteHookEvent(Tag deletedTag) {
return new TagHookContextProvider(emptyList(), singletonList(deletedTag));
}
@Override
public Set<HookFeature> getSupportedFeatures() {
return singleton(HookFeature.TAG_PROVIDER);
@@ -208,7 +220,30 @@ public class GitTagCommand extends AbstractGitCommand implements TagCommand {
@Override
public HookChangesetProvider getChangesetProvider() {
return r -> new HookChangesetResponse(emptyList());
Collection<ReceiveCommand> receiveCommands = new ArrayList<>();
newTags.stream()
.map(tag -> new ReceiveCommand(zeroId(), fromString(tag.getRevision()), REFS_TAGS_PREFIX + tag.getName()))
.forEach(receiveCommands::add);
deletedTags.stream()
.map(tag -> new ReceiveCommand(fromString(tag.getRevision()), zeroId(), REFS_TAGS_PREFIX + tag.getName()))
.forEach(receiveCommands::add);
return x -> {
Repository gitRepo;
try {
gitRepo = context.open();
} catch (IOException e) {
throw new InternalRepositoryException(repository, "failed to open repository for post receive hook after internal change", e);
}
GitHookChangesetCollector collector =
GitHookChangesetCollector.collectChangesets(
converterFactory,
receiveCommands,
gitRepo,
new RevWalk(gitRepo),
commit -> false // we cannot create new commits with this tag command
);
return new HookChangesetResponse(collector.getAddedChangesets(), collector.getRemovedChangesets());
};
}
}
}

View File

@@ -24,6 +24,8 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -32,9 +34,14 @@ import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.api.HookChangesetBuilder;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
@@ -48,14 +55,30 @@ 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 {
@Mock
private PreProcessorUtil preProcessorUtil;
private HookContextFactory hookContextFactory;
@Mock
private ScmEventBus eventBus;
@Mock
private GitChangesetConverterFactory converterFactory;
@Before
public void mockConverterFactory() {
GitChangesetConverter gitChangesetConverter = mock(GitChangesetConverter.class);
when(converterFactory.create(any(), any()))
.thenReturn(gitChangesetConverter);
when(gitChangesetConverter.createChangeset(any(), (String[]) any()))
.thenAnswer(invocation -> {
RevCommit revCommit = invocation.getArgument(0, RevCommit.class);
Changeset changeset = new Changeset(revCommit.name(), null, null);
return changeset;
});
hookContextFactory = new HookContextFactory(preProcessorUtil);
}
@Test
public void shouldCreateBranchWithDefinedSourceBranch() throws IOException {
@@ -108,7 +131,7 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
}
private GitBranchCommand createCommand() {
return new GitBranchCommand(createContext(), hookContextFactory, eventBus);
return new GitBranchCommand(createContext(), hookContextFactory, eventBus, converterFactory);
}
private List<Branch> readBranches(GitContext context) throws IOException {
@@ -119,7 +142,6 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
public void shouldPostCreateEvents() {
ArgumentCaptor<Object> 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");
@@ -140,7 +162,6 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
public void shouldPostDeleteEvents() {
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(captor.capture());
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
createCommand().deleteOrClose("squash");
@@ -151,11 +172,15 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
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;
HookChangesetBuilder changesetProvider = event.getContext().getChangesetProvider();
assertThat(changesetProvider.getChangesets()).isEmpty();
assertThat(changesetProvider.getRemovedChangesets())
.extracting("id")
.containsExactly(
"35597e9e98fe53167266583848bfef985c2adb27",
"f360a8738e4a29333786c5817f97a2c912814536",
"d1dfecbfd5b4a2f77fe40e1bde29e640f7f944be"
);
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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 org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitTestHelper;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
public class GitChangesetsCommandTest extends AbstractGitCommandTestBase {
@Test
public void shouldFindLatestCommit() {
GitChangesetsCommand command = new GitChangesetsCommand(createContext(), GitTestHelper.createConverterFactory());
Optional<Changeset> changeset = command.getLatestChangeset();
assertThat(changeset).get().extracting("id").isEqualTo("a8495c0335a13e6e432df90b3727fa91943189a7");
}
@Test
public void shouldListAllRevisions() {
GitChangesetsCommand command = new GitChangesetsCommand(createContext(), GitTestHelper.createConverterFactory());
Iterable<Changeset> changesets = command.getChangesets(new ChangesetsCommandRequest());
assertThat(changesets)
.extracting("id")
.hasSize(19)
.contains(
"a8495c0335a13e6e432df90b3727fa91943189a7",
"03ca33468c2094249973d0ca11b80243a20de368",
"9e93d8631675a89615fac56b09209686146ff3c0",
"383b954b27e052db6880d57f1c860dc208795247",
"1fcebf45a215a43f0713a57b807d55e8387a6d70",
"9f28cf5eb3a4df05d284c6f2d276c20c0f0e5b6c",
"674ca9a2208df60224b0f33beeea5259b374d2d0",
"35597e9e98fe53167266583848bfef985c2adb27",
"f360a8738e4a29333786c5817f97a2c912814536",
"d1dfecbfd5b4a2f77fe40e1bde29e640f7f944be",
"d81ad6c63d7e2162308d69637b339dedd1d9201c",
"6a2abf9dca5cff5d76720d1276e1112d9c75ea60",
"2f95f02d9c568594d31e78464bd11a96c62e3f91",
"fcd0ef1831e4002ac43ea539f4094334c79ea9ec",
"86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1",
"91b99de908fcd04772798a31c308a64aea1a5523",
"3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4",
"592d797cd36432e591416e8b2b98154f4f163411",
"435df2f061add3589cb326cc64be9b9c3897ceca"
);
}
}

View File

@@ -0,0 +1,211 @@
/*
* 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.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.web.CollectingPackParserListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.util.Arrays.asList;
import static org.eclipse.jgit.lib.ObjectId.fromString;
import static org.eclipse.jgit.lib.ObjectId.zeroId;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class GitHookChangesetCollectorTest extends AbstractGitCommandTestBase {
private final ReceivePack rpack = mock(ReceivePack.class);
private final Collection<ReceiveCommand> receiveCommands = new ArrayList<>();
private final CollectingPackParserListener listener = mock(CollectingPackParserListener.class);
private final GitChangesetConverterFactory converterFactory = mock(GitChangesetConverterFactory.class);
private final GitChangesetConverter converter = mock(GitChangesetConverter.class);
private GitHookChangesetCollector collector;
@Before
public void init() throws IOException {
GitContext context = createContext();
Repository repository = context.open();
RevWalk revWalk = new RevWalk(repository);
when(rpack.getRepository()).thenReturn(repository);
when(rpack.getRevWalk()).thenReturn(revWalk);
when(rpack.getPackParserListener()).thenReturn(listener);
when(converterFactory.create(repository, revWalk)).thenReturn(converter);
when(converter.createChangeset(any(), (String[]) any()))
.thenAnswer(invocation -> new Changeset(invocation.getArgument(0, RevCommit.class).name(), null, null));
}
@Test
public void shouldCreateEmptyCollectionsWithoutChanges() {
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets()).isEmpty();
assertThat(collector.getRemovedChangesets()).isEmpty();
}
@Test
public void shouldFindAddedChangesetsFromNewBranch() {
receiveCommands.add(
new ReceiveCommand(
zeroId(),
fromString("91b99de908fcd04772798a31c308a64aea1a5523"),
"refs/heads/mergeable")
);
mockNewCommits(
"91b99de908fcd04772798a31c308a64aea1a5523",
"3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4",
"592d797cd36432e591416e8b2b98154f4f163411");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets())
.extracting("id")
.contains(
"91b99de908fcd04772798a31c308a64aea1a5523",
"592d797cd36432e591416e8b2b98154f4f163411");
assertThat(collector.getRemovedChangesets()).isEmpty();
}
@Test
public void shouldFindAddedChangesetsFromNewBranchesOnce() throws IOException, GitAPIException {
new Git(createContext().open()).branchCreate().setStartPoint("mergeable").setName("second").call();
receiveCommands.add(
new ReceiveCommand(
zeroId(),
fromString("91b99de908fcd04772798a31c308a64aea1a5523"),
"refs/heads/mergeable")
);
receiveCommands.add(
new ReceiveCommand(
zeroId(),
fromString("91b99de908fcd04772798a31c308a64aea1a5523"),
"refs/heads/second")
);
mockNewCommits(
"91b99de908fcd04772798a31c308a64aea1a5523",
"3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4",
"592d797cd36432e591416e8b2b98154f4f163411");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets())
.extracting("id")
.hasSize(2)
.contains(
"91b99de908fcd04772798a31c308a64aea1a5523",
"592d797cd36432e591416e8b2b98154f4f163411");
assertThat(collector.getRemovedChangesets()).isEmpty();
}
@Test
public void shouldFindAddedChangesetsFromChangedBranchWithoutIteratingOldCommits() {
receiveCommands.add(
new ReceiveCommand(
fromString("592d797cd36432e591416e8b2b98154f4f163411"),
fromString("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"),
"refs/heads/test-branch")
);
mockNewCommits("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets())
.extracting("id")
.contains("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
assertThat(collector.getRemovedChangesets()).isEmpty();
verify(listener, never()).isNew(argThat(argument -> argument.name().equals("592d797cd36432e591416e8b2b98154f4f163411")));
}
@Test
public void shouldFindRemovedChangesetsFromDeletedBranch() throws IOException, GitAPIException {
new Git(createContext().open()).branchDelete().setBranchNames("test-branch").setForce(true).call();
receiveCommands.add(
new ReceiveCommand(
fromString("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"),
zeroId(),
"refs/heads/test-branch",
ReceiveCommand.Type.DELETE)
);
mockNewCommits("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets()).isEmpty();
assertThat(collector.getRemovedChangesets())
.extracting("id")
.contains("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
verify(listener, never()).isNew(argThat(argument -> argument.name().equals("592d797cd36432e591416e8b2b98154f4f163411")));
}
@Test
public void shouldFindRemovedAndAddedChangesetsFromNonFastForwardChanged() throws IOException, GitAPIException {
new Git(createContext().open()).branchDelete().setBranchNames("test-branch").setForce(true).call();
receiveCommands.add(
new ReceiveCommand(
fromString("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"),
fromString("91b99de908fcd04772798a31c308a64aea1a5523"),
"refs/heads/test-branch",
ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
);
mockNewCommits("91b99de908fcd04772798a31c308a64aea1a5523");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets())
.extracting("id")
.contains("91b99de908fcd04772798a31c308a64aea1a5523");
assertThat(collector.getRemovedChangesets())
.extracting("id")
.contains("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
}
private void mockNewCommits(String... objectIds) {
when(listener.isNew(any()))
.thenAnswer(invocation -> asList(objectIds).contains(invocation.getArgument(0, RevCommit.class).name()));
}
}

View File

@@ -28,24 +28,33 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.After;
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.Changeset;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookChangesetBuilder;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.TagDeleteRequest;
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.util.MockUtil;
@@ -66,12 +75,15 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
private GPG gpg;
@Mock
private PreProcessorUtil preProcessorUtil;
private HookContextFactory hookContextFactory;
@Mock
private ScmEventBus eventBus;
private Subject subject;
@Mock
private GitChangesetConverterFactory converterFactory;
@Before
public void setSigner() {
@@ -81,10 +93,24 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
@Before
public void bindThreadContext() {
SecurityUtils.setSecurityManager(new DefaultSecurityManager());
subject = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
Subject subject = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
ThreadContext.bind(subject);
}
@Before
public void mockConverterFactory() {
GitChangesetConverter gitChangesetConverter = mock(GitChangesetConverter.class);
when(converterFactory.create(any(), any()))
.thenReturn(gitChangesetConverter);
when(gitChangesetConverter.createChangeset(any(), (String[]) any()))
.thenAnswer(invocation -> {
RevCommit revCommit = invocation.getArgument(0, RevCommit.class);
Changeset changeset = new Changeset(revCommit.name(), null, null);
return changeset;
});
hookContextFactory = new HookContextFactory(preProcessorUtil);
}
@After
public void unbindThreadContext() {
ThreadContext.unbindSubject();
@@ -105,7 +131,6 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
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"));
@@ -131,26 +156,36 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
}
@Test
public void shouldPostDeleteEvent() {
public void shouldPostDeleteEvent() throws IOException, GitAPIException {
Git git = new Git(createContext().open());
git.tag().setName("to-be-deleted").setObjectId(getCommit("383b954b27e052db6880d57f1c860dc208795247")).call();
git.branchDelete().setBranchNames("rename").setForce(true).call();
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"));
createCommand().delete(new TagDeleteRequest("to-be-deleted"));
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");
HookTagProvider tagProvider = event.getContext().getTagProvider();
assertThat(tagProvider.getCreatedTags()).isEmpty();
Tag deletedTag = tagProvider.getDeletedTags().get(0);
assertThat(deletedTag.getName()).isEqualTo("to-be-deleted");
assertThat(deletedTag.getRevision()).isEqualTo("383b954b27e052db6880d57f1c860dc208795247");
HookChangesetBuilder changesetProvider = event.getContext().getChangesetProvider();
assertThat(changesetProvider.getChangesets()).isEmpty();
assertThat(changesetProvider.getRemovedChangesets())
.extracting("id")
.containsExactly("383b954b27e052db6880d57f1c860dc208795247");
}
private GitTagCommand createCommand() {
return new GitTagCommand(createContext(), hookContextFactory, eventBus);
return new GitTagCommand(createContext(), hookContextFactory, eventBus, converterFactory);
}
private List<Tag> readTags(GitContext context) throws IOException {
@@ -162,9 +197,10 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
return tags.stream().filter(t -> name.equals(t.getName())).findFirst();
}
private HookContext createMockedContext(InvocationOnMock invocation) {
HookContext mock = mock(HookContext.class);
when(mock.getTagProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getTagProvider());
return mock;
private RevCommit getCommit(String revision) throws IOException {
ObjectId commitId = ObjectId.fromString(revision);
try (RevWalk revWalk = new RevWalk(createContext().open())) {
return revWalk.parseCommit(commitId);
}
}
}