Add a modifications provider for hooks

This new modifications provider consistently computes the modifications caused by a push to a branch. In contrast to the changeset provider that often has been used before to check what has changed, this also works for forced updates, rebased branches and fast-forwards.

Because these types of changes are normally only used with git, this provider (for now) has only been implemented for git.

Pushed-by: Rene Pfeuffer<rene.pfeuffer@cloudogu.com>
Pushed-by: Alexander Dammeier<alexander.dammeier@cloudogu.com>
Co-authored-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>
Committed-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>
This commit is contained in:
Rene Pfeuffer
2023-11-16 13:18:40 +01:00
parent a8c32b10de
commit 571e6ad92f
32 changed files with 857 additions and 364 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Modifications in hook provider

View File

@@ -34,7 +34,6 @@ import sonia.scm.io.DeepCopy;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.HookChangesetProvider;
import sonia.scm.repository.spi.HookChangesetRequest;
import sonia.scm.repository.spi.HookChangesetResponse;

View File

@@ -22,7 +22,10 @@
* SOFTWARE.
*/
package sonia.scm.repository.spi;
package sonia.scm.repository.api;
import sonia.scm.repository.spi.HookChangesetRequest;
import sonia.scm.repository.spi.HookChangesetResponse;
/**
* @author Sebastian Sdorra

View File

@@ -80,8 +80,7 @@ public final class HookContext {
* @since 1.45
*/
public HookBranchProvider getBranchProvider() {
logger.debug("create branch provider for repository {}",
repository.getName());
logger.debug("create branch provider for repository {}", repository);
return provider.getBranchProvider();
}
@@ -98,8 +97,7 @@ public final class HookContext {
* @since 1.50
*/
public HookTagProvider getTagProvider() {
logger.debug("create tag provider for repository {}",
repository.getName());
logger.debug("create tag provider for repository {}", repository);
return provider.getTagProvider();
}
@@ -115,8 +113,7 @@ public final class HookContext {
* by the underlying provider
*/
public HookChangesetBuilder getChangesetProvider() {
logger.debug("create changeset provider for repository {}",
repository.getName());
logger.debug("create changeset provider for repository {}", repository);
return new HookChangesetBuilder(
repository,
@@ -125,6 +122,22 @@ public final class HookContext {
);
}
/**
* Returns a {@link HookChangesetBuilder} which is able to return all
* {@link Changeset}'s during this push/commit.
*
*
* @return {@link HookChangesetBuilder}
*
* @throws HookFeatureIsNotSupportedException if the feature is not supported
* by the underlying provider
*/
public HookModificationsProvider getModificationsProvider() {
logger.debug("create diff provider for repository {}", repository);
return provider.getModificationsProvider();
}
/**
* Returns a {@link HookMessageProvider} which is able to send message back to
* the scm client.
@@ -139,8 +152,7 @@ public final class HookContext {
* by the underlying provider
*/
public HookMessageProvider getMessageProvider() {
logger.debug("create message provider for repository {}",
repository.getName());
logger.debug("create message provider for repository {}", repository);
return provider.getMessageProvider();
}
@@ -155,8 +167,7 @@ public final class HookContext {
* by the underlying provider
*/
public HookMergeDetectionProvider getMergeDetectionProvider() {
logger.debug("create merge detection provider for repository {}",
repository.getName());
logger.debug("create merge detection provider for repository {}", repository);
return provider.getMergeDetectionProvider();
}

View File

@@ -62,5 +62,12 @@ public enum HookFeature
*
* @since 2.4.0
*/
MERGE_DETECTION_PROVIDER
MERGE_DETECTION_PROVIDER,
/**
* Provider to compute modifications
*
* @since 2.48.0
*/
MODIFICATIONS_PROVIDER
}

View File

@@ -21,14 +21,23 @@
* 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.ContextEntry;
import sonia.scm.repository.InternalRepositoryException;
package sonia.scm.repository.api;
public class UnsupportedModificationTypeException extends InternalRepositoryException {
public UnsupportedModificationTypeException(ContextEntry.ContextBuilder entity, String message) {
super(entity, message);
}
import sonia.scm.repository.Modifications;
/**
* The HookDiffProvider returns modifications of branches that have been changed during the current hook.
*
* @since 2.48.0
*/
public interface HookModificationsProvider {
/**
* If the given branch has been updated during the current hook, this method returns an {@link Modifications} instance
* with the modifications of the branch.
* If the branch has been deleted, this will return {@link sonia.scm.repository.Removed} modifications for all paths.
* Accordingly, if the branch has been created, this will return {@link sonia.scm.repository.Added} modifications
* for all paths.
*/
Modifications getModifications(String branchName);
}

View File

@@ -24,18 +24,16 @@
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.api.HookBranchProvider;
import sonia.scm.repository.api.HookChangesetProvider;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookException;
import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookFeatureIsNotSupportedException;
import sonia.scm.repository.api.HookMessageProvider;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookModificationsProvider;
import sonia.scm.repository.api.HookTagProvider;
//~--- JDK imports ------------------------------------------------------------
import java.util.Set;
/**
@@ -128,6 +126,10 @@ public abstract class HookContextProvider
throw new HookFeatureIsNotSupportedException(HookFeature.MERGE_DETECTION_PROVIDER);
}
public HookModificationsProvider getModificationsProvider() {
throw new HookFeatureIsNotSupportedException(HookFeature.MODIFICATIONS_PROVIDER);
}
//~--- methods --------------------------------------------------------------
/**

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.api;
import com.google.common.collect.Lists;
@@ -36,7 +36,6 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.Person;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.HookChangesetProvider;
import sonia.scm.repository.spi.HookChangesetRequest;
import sonia.scm.repository.spi.HookChangesetResponse;
import sonia.scm.repository.spi.HookContextProvider;
@@ -53,7 +52,7 @@ import static org.mockito.Mockito.when;
/**
* Unit tests for {@link HookContext}.
*
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
@@ -61,19 +60,19 @@ public class HookContextTest {
@Mock
private HookContextProvider provider;
@Mock
private Repository repository;
@Mock
private PreProcessorUtil preProcessorUtil;
@Mock
private HookChangesetProvider changesetProvider;
@InjectMocks
private HookContext context;
/**
* Set up mocks for upcoming test.
*/
@@ -81,19 +80,19 @@ public class HookContextTest {
public void setUpMocks(){
when(provider.getChangesetProvider()).thenReturn(changesetProvider);
when(provider.getSupportedFeatures()).thenReturn(Sets.newHashSet(HookFeature.CHANGESET_PROVIDER));
List<Changeset> changesets = Lists.newArrayList(new Changeset("1", Long.MIN_VALUE, new Person("Trillian")));
HookChangesetResponse response = new HookChangesetResponse(changesets);
when(changesetProvider.handleRequest(any(HookChangesetRequest.class))).thenReturn(response);
}
/**
* Tests {@link HookContext#getBranchProvider()}.
*/
@Test
public void testGetBranchProvider() {
context.getBranchProvider();
verify(provider).getBranchProvider();
}
@@ -103,20 +102,20 @@ public class HookContextTest {
@Test
public void testGetTagProvider() {
context.getTagProvider();
verify(provider).getTagProvider();
}
/**
* Tests {@link HookContext#getMessageProvider()}.
*/
@Test
public void testGetMessageProvider() {
context.getMessageProvider();
verify(provider).getMessageProvider();
}
/**
* Tests {@link HookContext#getChangesetProvider()}.
*/
@@ -127,7 +126,7 @@ public class HookContextTest {
assertNotNull(changesets);
assertEquals("1", changesets.get(0).getId());
}
/**
* Tests {@link HookContext#isFeatureSupported(sonia.scm.repository.api.HookFeature)}.
*/
@@ -136,5 +135,5 @@ public class HookContextTest {
assertTrue(context.isFeatureSupported(HookFeature.CHANGESET_PROVIDER));
assertFalse(context.isFeatureSupported(HookFeature.BRANCH_PROVIDER));
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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.api;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import lombok.AllArgsConstructor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry;
import sonia.scm.repository.Added;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modification;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Removed;
import sonia.scm.repository.spi.BranchBasedModificationsComputer;
import sonia.scm.repository.spi.ModificationsComputer;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* Computes modifications for created, modified and deleted git branches during a hook.
*/
public class GitHookModificationsProvider implements HookModificationsProvider {
private static final Logger logger = LoggerFactory.getLogger(GitHookModificationsProvider.class);
private final org.eclipse.jgit.lib.Repository repository;
private final Map<String, BranchEntry> modificationsCommandRequests;
public GitHookModificationsProvider(List<ReceiveCommand> commands, org.eclipse.jgit.lib.Repository repository) {
this.repository = repository;
ImmutableMap.Builder<String, BranchEntry> modificationsCommandRequestBuilder = ImmutableMap.builder();
for (ReceiveCommand command : commands) {
String ref = command.getRefName();
String branch = GitUtil.getBranch(ref);
if (Strings.isNullOrEmpty(branch)) {
logger.debug("ref {} is not a branch", ref);
} else if (command.getType() == ReceiveCommand.Type.UPDATE || command.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD) {
modificationsCommandRequestBuilder.put(branch, new UpdateBranchEntry(command.getNewId().name(), command.getOldId().name()));
} else if (command.getType() == ReceiveCommand.Type.CREATE) {
modificationsCommandRequestBuilder.put(branch, new CreateBranchEntry(command.getNewId()));
} else if (command.getType() == ReceiveCommand.Type.DELETE) {
modificationsCommandRequestBuilder.put(branch, new DeleteBranchEntry(command.getOldId()));
}
}
modificationsCommandRequests = modificationsCommandRequestBuilder.build();
}
@Override
public Modifications getModifications(String branchName) {
BranchEntry branchEntry = modificationsCommandRequests.get(branchName);
try {
return branchEntry.getModifications();
} catch (IOException ex) {
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Git Repository", repository.toString()), "could not compute diff for branch " + branchName, ex);
}
}
private interface BranchEntry {
Modifications getModifications() throws IOException;
}
@AllArgsConstructor
private class UpdateBranchEntry implements BranchEntry {
private final String newRevision;
private final String oldRevision;
@Override
public Modifications getModifications() throws IOException {
return new Modifications(
oldRevision,
newRevision,
new ModificationsComputer(repository).compute(oldRevision, newRevision).getModifications()
);
}
}
@AllArgsConstructor
private class DeleteBranchEntry implements BranchEntry {
private final ObjectId oldRevision;
@Override
public Modifications getModifications() throws IOException {
return createModifications(oldRevision, Removed::new);
}
}
@AllArgsConstructor
private class CreateBranchEntry implements BranchEntry {
private final ObjectId newRevision;
@Override
public Modifications getModifications() throws IOException {
return createModifications(newRevision, Added::new);
}
}
private Modifications createModifications(ObjectId revision, Function<String, Modification> modificationFactory) throws IOException {
return new BranchBasedModificationsComputer(repository).createModifications(revision, modificationFactory);
}
}

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 org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import sonia.scm.repository.Modification;
import sonia.scm.repository.Modifications;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Function;
public class BranchBasedModificationsComputer {
private final Repository repository;
public BranchBasedModificationsComputer(Repository repository) {
this.repository = repository;
}
public Modifications createModifications(ObjectId revision, Function<String, Modification> modificationFactory) throws IOException {
Collection<Modification> modifications = new ArrayList<>();
try (RevWalk revWalk = new RevWalk(repository); TreeWalk treeWalk = new TreeWalk(repository)) {
RevTree tree = revWalk.parseTree(revision);
treeWalk.addTree(tree);
while (treeWalk.next()) {
if (treeWalk.isSubtree()) {
treeWalk.enterSubtree();
} else {
modifications.add(modificationFactory.apply(treeWalk.getPathString()));
}
}
}
return new Modifications(revision.name(), modifications);
}
}

View File

@@ -46,7 +46,7 @@ import java.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
final class Differ implements AutoCloseable {
public final class Differ implements AutoCloseable {
private final RevWalk walk;
private final TreeWalk treeWalk;
@@ -62,7 +62,7 @@ final class Differ implements AutoCloseable {
this.commonAncestor = commonAncestor;
}
static Diff diff(Repository repository, DiffCommandRequest request) throws IOException {
public static Diff diff(Repository repository, DiffCommandRequest request) throws IOException {
try (Differ differ = create(repository, request)) {
return differ.diff(repository, differ.commonAncestor);
}

View File

@@ -35,31 +35,36 @@ 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.ContextEntry;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Added;
import sonia.scm.repository.Branch;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modification;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.Removed;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.api.HookBranchProvider;
import sonia.scm.repository.api.HookChangesetProvider;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookModificationsProvider;
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 java.util.function.Function;
import static java.util.Arrays.asList;
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;
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
@@ -138,43 +143,29 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
}
private BranchHookContextProvider createHookEvent(String newBranch, ObjectId objectId) {
return new BranchHookContextProvider(singletonList(newBranch), emptyList(), objectId);
return new CreatedBranchHookContextProvider(newBranch, objectId);
}
private BranchHookContextProvider deleteHookEvent(String deletedBranch, ObjectId oldObjectId) {
return new BranchHookContextProvider(emptyList(), singletonList(deletedBranch), oldObjectId);
return new DeletedBranchHookContextProvider(deletedBranch, oldObjectId);
}
private class BranchHookContextProvider extends HookContextProvider {
private final List<String> newBranches;
private final List<String> deletedBranches;
private final ObjectId objectId;
private abstract class BranchHookContextProvider extends HookContextProvider {
final String branchName;
final ObjectId objectId;
private BranchHookContextProvider(List<String> newBranches, List<String> deletedBranches, ObjectId objectId) {
this.newBranches = newBranches;
this.deletedBranches = deletedBranches;
private BranchHookContextProvider(String branchName, ObjectId objectId) {
this.branchName = branchName;
this.objectId = objectId;
}
@Override
public Set<HookFeature> getSupportedFeatures() {
return singleton(HookFeature.BRANCH_PROVIDER);
return Set.of(HookFeature.BRANCH_PROVIDER, HookFeature.MODIFICATIONS_PROVIDER, HookFeature.CHANGESET_PROVIDER);
}
@Override
public HookBranchProvider getBranchProvider() {
return new HookBranchProvider() {
@Override
public List<String> getCreatedOrModified() {
return newBranches;
}
@Override
public List<String> getDeletedOrClosed() {
return deletedBranches;
}
};
}
public abstract HookBranchProvider getBranchProvider();
@Override
public HookChangesetProvider getChangesetProvider() {
@@ -185,13 +176,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
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);
Collection<ReceiveCommand> receiveCommands = asList(createReceiveCommand());
return x -> {
GitHookChangesetCollector collector =
GitHookChangesetCollector.collectChangesets(
@@ -204,6 +189,83 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
return new HookChangesetResponse(collector.getAddedChangesets(), collector.getRemovedChangesets());
};
}
abstract ReceiveCommand createReceiveCommand();
@Override
public HookModificationsProvider getModificationsProvider() {
return branchName -> {
try {
return new BranchBasedModificationsComputer(context.open()).createModifications(objectId, getModificationFactory());
} catch (IOException ex) {
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Git Repository", repository.toString()), "could not compute diff for branch " + branchName, ex);
}
};
}
abstract Function<String, Modification> getModificationFactory();
}
private class CreatedBranchHookContextProvider extends BranchHookContextProvider {
public CreatedBranchHookContextProvider(String branchName, ObjectId objectId) {
super(branchName, objectId);
}
@Override
public HookBranchProvider getBranchProvider() {
return new HookBranchProvider() {
@Override
public List<String> getCreatedOrModified() {
return asList(branchName);
}
@Override
public List<String> getDeletedOrClosed() {
return emptyList();
}
};
}
@Override
ReceiveCommand createReceiveCommand() {
return new ReceiveCommand(zeroId(), objectId, "refs/heads/" + branchName);
}
@Override
Function<String, Modification> getModificationFactory() {
return Added::new;
}
}
private class DeletedBranchHookContextProvider extends BranchHookContextProvider {
public DeletedBranchHookContextProvider(String branchName, ObjectId objectId) {
super(branchName, objectId);
}
@Override
public HookBranchProvider getBranchProvider() {
return new HookBranchProvider() {
@Override
public List<String> getCreatedOrModified() {
return emptyList();
}
@Override
public List<String> getDeletedOrClosed() {
return asList(branchName);
}
};
}
@Override
ReceiveCommand createReceiveCommand() {
return new ReceiveCommand(objectId, zeroId(), "refs/heads/" + branchName);
}
@Override
Function<String, Modification> getModificationFactory() {
return Removed::new;
}
}
public interface Factory {

View File

@@ -0,0 +1,175 @@
/*
* 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.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.DiffFile;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.Hunk;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static java.util.Optional.ofNullable;
public class GitDiffResult implements DiffResult {
private final Repository scmRepository;
private final org.eclipse.jgit.lib.Repository repository;
private final Differ.Diff diff;
private final List<DiffEntry> diffEntries;
private final int offset;
private final Integer limit;
public GitDiffResult(Repository scmRepository, org.eclipse.jgit.lib.Repository repository, Differ.Diff diff, int offset, Integer limit) {
this.scmRepository = scmRepository;
this.repository = repository;
this.diff = diff;
this.offset = offset;
this.limit = limit;
this.diffEntries = diff.getEntries();
}
@Override
public String getOldRevision() {
ObjectId commonAncestor = diff.getCommonAncestor();
if (commonAncestor != null) {
return commonAncestor.name();
}
return diff.getCommit().getParentCount() > 0 ? GitUtil.getId(diff.getCommit().getParent(0).getId()) : null;
}
@Override
public String getNewRevision() {
return GitUtil.getId(diff.getCommit().getId());
}
@Override
public boolean isPartial() {
return limit != null && limit + offset < diffEntries.size();
}
@Override
public int getOffset() {
return offset;
}
@Override
public Optional<Integer> getLimit() {
return ofNullable(limit);
}
@Override
public Iterator<DiffFile> iterator() {
Stream<DiffEntry> diffEntryStream = diffEntries
.stream()
.skip(offset);
if (limit != null) {
diffEntryStream = diffEntryStream.limit(limit);
}
return diffEntryStream
.map(diffEntry -> new GitDiffFile(repository, diffEntry))
.map(DiffFile.class::cast)
.iterator();
}
private class GitDiffFile implements DiffFile {
private final org.eclipse.jgit.lib.Repository repository;
private final DiffEntry diffEntry;
private GitDiffFile(org.eclipse.jgit.lib.Repository repository, DiffEntry diffEntry) {
this.repository = repository;
this.diffEntry = diffEntry;
}
@Override
public String getOldRevision() {
return GitDiffResult.this.getOldRevision();
}
@Override
public String getNewRevision() {
return GitDiffResult.this.getNewRevision();
}
@Override
public String getOldPath() {
return diffEntry.getOldPath();
}
@Override
public String getNewPath() {
return diffEntry.getNewPath();
}
@Override
public ChangeType getChangeType() {
switch (diffEntry.getChangeType()) {
case ADD:
return ChangeType.ADD;
case MODIFY:
return ChangeType.MODIFY;
case RENAME:
return ChangeType.RENAME;
case DELETE:
return ChangeType.DELETE;
case COPY:
return ChangeType.COPY;
default:
throw new IllegalArgumentException("Unknown change type: " + diffEntry.getChangeType());
}
}
@Override
public Iterator<Hunk> iterator() {
String content = format(repository, diffEntry);
GitHunkParser parser = new GitHunkParser();
return parser.parse(content).iterator();
}
private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DiffFormatter formatter = new DiffFormatter(baos)) {
formatter.setRepository(repository);
formatter.format(entry);
return baos.toString(StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new InternalRepositoryException(scmRepository, "failed to format diff entry", ex);
}
}
}
}

View File

@@ -25,28 +25,13 @@
package sonia.scm.repository.spi;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.NotUniqueRevisionException;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.DiffFile;
import sonia.scm.repository.api.DiffResult;
import sonia.scm.repository.api.Hunk;
import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static java.util.Optional.ofNullable;
public class GitDiffResultCommand extends AbstractGitCommand implements DiffResultCommand {
@@ -57,7 +42,7 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu
public DiffResult getDiffResult(DiffCommandRequest diffCommandRequest) throws IOException {
org.eclipse.jgit.lib.Repository repository = open();
return new GitDiffResult(repository, Differ.diff(repository, diffCommandRequest), 0, null);
return new GitDiffResult(this.repository, repository, Differ.diff(repository, diffCommandRequest), 0, null);
}
@Override
@@ -65,140 +50,12 @@ public class GitDiffResultCommand extends AbstractGitCommand implements DiffResu
org.eclipse.jgit.lib.Repository repository = open();
int offset = request.getOffset() == null ? 0 : request.getOffset();
try {
return new GitDiffResult(repository, Differ.diff(repository, request), offset, request.getLimit());
return new GitDiffResult(this.repository, repository, Differ.diff(repository, request), offset, request.getLimit());
} catch (AmbiguousObjectException ex) {
throw new NotUniqueRevisionException(Repository.class, context.getRepository().getId());
}
}
private class GitDiffResult implements DiffResult {
private final org.eclipse.jgit.lib.Repository repository;
private final Differ.Diff diff;
private final List<DiffEntry> diffEntries;
private final int offset;
private final Integer limit;
private GitDiffResult(org.eclipse.jgit.lib.Repository repository, Differ.Diff diff, int offset, Integer limit) {
this.repository = repository;
this.diff = diff;
this.offset = offset;
this.limit = limit;
this.diffEntries = diff.getEntries();
}
@Override
public String getOldRevision() {
ObjectId commonAncestor = diff.getCommonAncestor();
if (commonAncestor != null) {
return commonAncestor.name();
}
return diff.getCommit().getParentCount() > 0 ? GitUtil.getId(diff.getCommit().getParent(0).getId()) : null;
}
@Override
public String getNewRevision() {
return GitUtil.getId(diff.getCommit().getId());
}
@Override
public boolean isPartial() {
return limit != null && limit + offset < diffEntries.size();
}
@Override
public int getOffset() {
return offset;
}
@Override
public Optional<Integer> getLimit() {
return ofNullable(limit);
}
@Override
public Iterator<DiffFile> iterator() {
Stream<DiffEntry> diffEntryStream = diffEntries
.stream()
.skip(offset);
if (limit != null) {
diffEntryStream = diffEntryStream.limit(limit);
}
return diffEntryStream
.map(diffEntry -> new GitDiffFile(repository, diffEntry))
.map(DiffFile.class::cast)
.iterator();
}
private class GitDiffFile implements DiffFile {
private final org.eclipse.jgit.lib.Repository repository;
private final DiffEntry diffEntry;
private GitDiffFile(org.eclipse.jgit.lib.Repository repository, DiffEntry diffEntry) {
this.repository = repository;
this.diffEntry = diffEntry;
}
@Override
public String getOldRevision() {
return GitDiffResult.this.getOldRevision();
}
@Override
public String getNewRevision() {
return GitDiffResult.this.getNewRevision();
}
@Override
public String getOldPath() {
return diffEntry.getOldPath();
}
@Override
public String getNewPath() {
return diffEntry.getNewPath();
}
@Override
public ChangeType getChangeType() {
switch (diffEntry.getChangeType()) {
case ADD:
return ChangeType.ADD;
case MODIFY:
return ChangeType.MODIFY;
case RENAME:
return ChangeType.RENAME;
case DELETE:
return ChangeType.DELETE;
case COPY:
return ChangeType.COPY;
default:
throw new IllegalArgumentException("Unknown change type: " + diffEntry.getChangeType());
}
}
@Override
public Iterator<Hunk> iterator() {
String content = format(repository, diffEntry);
GitHunkParser parser = new GitHunkParser();
return parser.parse(content).iterator();
}
private String format(org.eclipse.jgit.lib.Repository repository, DiffEntry entry) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DiffFormatter formatter = new DiffFormatter(baos)) {
formatter.setRepository(repository);
formatter.format(entry);
return baos.toString(StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new InternalRepositoryException(GitDiffResultCommand.this.repository, "failed to format diff entry", ex);
}
}
}
}
public interface Factory {
DiffResultCommand create(GitContext context);
}

View File

@@ -27,6 +27,7 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.api.HookChangesetProvider;
import java.util.List;

View File

@@ -31,10 +31,13 @@ import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
import sonia.scm.repository.api.GitHookBranchProvider;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.api.GitHookModificationsProvider;
import sonia.scm.repository.api.GitHookMessageProvider;
import sonia.scm.repository.api.GitHookTagProvider;
import sonia.scm.repository.api.GitReceiveHookMergeDetectionProvider;
import sonia.scm.repository.api.HookBranchProvider;
import sonia.scm.repository.api.HookChangesetProvider;
import sonia.scm.repository.api.HookModificationsProvider;
import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookMessageProvider;
import sonia.scm.repository.api.HookTagProvider;
@@ -58,6 +61,7 @@ public class GitHookContextProvider extends HookContextProvider
HookFeature.CHANGESET_PROVIDER,
HookFeature.BRANCH_PROVIDER,
HookFeature.TAG_PROVIDER,
HookFeature.MODIFICATIONS_PROVIDER,
HookFeature.MERGE_DETECTION_PROVIDER
);
@@ -117,6 +121,11 @@ public class GitHookContextProvider extends HookContextProvider
return new GitReceiveHookMergeDetectionProvider(repository, repositoryId, receiveCommands, converterFactory);
}
@Override
public HookModificationsProvider getModificationsProvider() {
return new GitHookModificationsProvider(receiveCommands, repository);
}
@Override
public Set<HookFeature> getSupportedFeatures()
{

View File

@@ -28,6 +28,7 @@ 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.HookChangesetProvider;
import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookTagProvider;

View File

@@ -26,30 +26,11 @@ package sonia.scm.repository.spi;
import com.google.inject.assistedinject.Assisted;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import sonia.scm.repository.Added;
import sonia.scm.repository.Copied;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Modification;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Modified;
import sonia.scm.repository.Removed;
import sonia.scm.repository.Renamed;
import javax.inject.Inject;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@@ -58,106 +39,27 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand {
@Inject
GitModificationsCommand(@Assisted GitContext context) {
public GitModificationsCommand(@Assisted GitContext context) {
super(context);
}
@Override
@SuppressWarnings("java:S2093")
public Modifications getModifications(String baseRevision, String revision) {
org.eclipse.jgit.lib.Repository gitRepository = null;
RevWalk revWalk = null;
try {
gitRepository = open();
if (!gitRepository.getAllRefs().isEmpty()) {
revWalk = new RevWalk(gitRepository);
RevCommit commit = getCommit(revision, gitRepository, revWalk);
TreeWalk treeWalk = createTreeWalk(gitRepository);
if (baseRevision == null) {
determineParentAsBase(treeWalk, commit, revWalk);
} else {
RevCommit baseCommit = getCommit(baseRevision, gitRepository, revWalk);
treeWalk.addTree(baseCommit.getTree());
}
return new Modifications(baseRevision, revision, createModifications(treeWalk, commit));
}
return new ModificationsComputer(open()).compute(baseRevision, revision);
} catch (IOException ex) {
log.error("could not open repository: " + repository.getNamespaceAndName(), ex);
throw new InternalRepositoryException(entity(repository), "could not open repository: " + repository.getNamespaceAndName(), ex);
} finally {
GitUtil.release(revWalk);
}
return null;
}
private RevCommit getCommit(String revision, Repository gitRepository, RevWalk revWalk) throws IOException {
ObjectId id = GitUtil.getRevisionId(gitRepository, revision);
return revWalk.parseCommit(id);
}
@Override
public Modifications getModifications(String revision) {
return getModifications(null, revision);
}
private TreeWalk createTreeWalk(Repository gitRepository) {
TreeWalk treeWalk = new TreeWalk(gitRepository);
treeWalk.reset();
treeWalk.setRecursive(true);
return treeWalk;
}
private Collection<Modification> createModifications(TreeWalk treeWalk, RevCommit commit)
throws IOException {
treeWalk.addTree(commit.getTree());
List<DiffEntry> entries = Differ.scanWithRename(context.open(), null, treeWalk);
Collection<Modification> modifications = new ArrayList<>();
for (DiffEntry e : entries) {
if (!e.getOldId().equals(e.getNewId()) || !e.getOldPath().equals(e.getNewPath())) {
modifications.add(asModification(e));
}
}
return modifications;
}
private void determineParentAsBase(TreeWalk treeWalk, RevCommit commit, RevWalk revWalk) throws IOException {
if (commit.getParentCount() > 0) {
RevCommit parent = commit.getParent(0);
RevTree tree = parent.getTree();
if ((tree == null) && (revWalk != null)) {
revWalk.parseHeaders(parent);
tree = parent.getTree();
}
if (tree != null) {
treeWalk.addTree(tree);
} else {
log.trace("no parent tree at position 0 for commit {}", commit.getName());
treeWalk.addTree(new EmptyTreeIterator());
}
} else {
log.trace("no parent available for commit {}", commit.getName());
treeWalk.addTree(new EmptyTreeIterator());
}
}
private Modification asModification(DiffEntry entry) throws UnsupportedModificationTypeException {
DiffEntry.ChangeType type = entry.getChangeType();
switch (type) {
case ADD:
return new Added(entry.getNewPath());
case MODIFY:
return new Modified(entry.getNewPath());
case DELETE:
return new Removed(entry.getOldPath());
case RENAME:
return new Renamed(entry.getOldPath(), entry.getNewPath());
case COPY:
return new Copied(entry.getOldPath(), entry.getNewPath());
default:
throw new UnsupportedModificationTypeException(entity(repository), MessageFormat.format("The modification type: {0} is not supported.", type));
}
}
public interface Factory {
ModificationsCommand create(GitContext context);
}

View File

@@ -48,6 +48,7 @@ import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.HookChangesetProvider;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.HookFeature;

View File

@@ -0,0 +1,143 @@
/*
* 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.diff.DiffEntry;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import sonia.scm.repository.Added;
import sonia.scm.repository.Copied;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.Modification;
import sonia.scm.repository.Modifications;
import sonia.scm.repository.Modified;
import sonia.scm.repository.Removed;
import sonia.scm.repository.Renamed;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Slf4j
public class ModificationsComputer {
private final Repository gitRepository;
public ModificationsComputer(Repository gitRepository) {
this.gitRepository = gitRepository;
}
public Modifications compute(String baseRevision, String revision) throws IOException {
RevWalk revWalk = null;
if (!gitRepository.getAllRefs().isEmpty()) {
try {
revWalk = new RevWalk(gitRepository);
RevCommit commit = getCommit(revision, gitRepository, revWalk);
TreeWalk treeWalk = createTreeWalk(gitRepository);
if (baseRevision == null) {
determineParentAsBase(treeWalk, commit, revWalk);
} else {
RevCommit baseCommit = getCommit(baseRevision, gitRepository, revWalk);
treeWalk.addTree(baseCommit.getTree());
}
return new Modifications(baseRevision, revision, createModifications(treeWalk, commit));
} finally {
GitUtil.release(revWalk);
}
}
return null;
}
private RevCommit getCommit(String revision, Repository gitRepository, RevWalk revWalk) throws IOException {
ObjectId id = GitUtil.getRevisionId(gitRepository, revision);
return revWalk.parseCommit(id);
}
private TreeWalk createTreeWalk(Repository gitRepository) {
TreeWalk treeWalk = new TreeWalk(gitRepository);
treeWalk.reset();
treeWalk.setRecursive(true);
return treeWalk;
}
private Collection<Modification> createModifications(TreeWalk treeWalk, RevCommit commit)
throws IOException {
treeWalk.addTree(commit.getTree());
List<DiffEntry> entries = Differ.scanWithRename(gitRepository, null, treeWalk);
Collection<Modification> modifications = new ArrayList<>();
for (DiffEntry e : entries) {
if (!e.getOldId().equals(e.getNewId()) || !e.getOldPath().equals(e.getNewPath())) {
modifications.add(asModification(e));
}
}
return modifications;
}
private void determineParentAsBase(TreeWalk treeWalk, RevCommit commit, RevWalk revWalk) throws IOException {
if (commit.getParentCount() > 0) {
RevCommit parent = commit.getParent(0);
RevTree tree = parent.getTree();
if ((tree == null) && (revWalk != null)) {
revWalk.parseHeaders(parent);
tree = parent.getTree();
}
if (tree != null) {
treeWalk.addTree(tree);
} else {
log.trace("no parent tree at position 0 for commit {}", commit.getName());
treeWalk.addTree(new EmptyTreeIterator());
}
} else {
log.trace("no parent available for commit {}", commit.getName());
treeWalk.addTree(new EmptyTreeIterator());
}
}
private Modification asModification(DiffEntry entry) {
DiffEntry.ChangeType type = entry.getChangeType();
switch (type) {
case ADD:
return new Added(entry.getNewPath());
case MODIFY:
return new Modified(entry.getNewPath());
case DELETE:
return new Removed(entry.getOldPath());
case RENAME:
return new Renamed(entry.getOldPath(), entry.getNewPath());
case COPY:
return new Copied(entry.getOldPath(), entry.getNewPath());
default:
throw new IllegalArgumentException(MessageFormat.format("The modification type: {0} is not supported.", type));
}
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.api;
import com.google.common.collect.Lists;
@@ -40,7 +40,7 @@ import org.mockito.junit.MockitoJUnitRunner;
/**
* Unit tests for {@link GitHookBranchProvider}.
*
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
@@ -48,9 +48,9 @@ public class GitHookBranchProviderTest {
@Mock
private ReceiveCommand command;
private List<ReceiveCommand> commands;
/**
* Prepare mocks for upcoming test.
*/
@@ -58,37 +58,37 @@ public class GitHookBranchProviderTest {
public void setUpMocks(){
commands = Lists.newArrayList(command);
}
/**
* Tests {@link GitHookBranchProvider#getCreatedOrModified()}.
*/
@Test
public void testGetCreatedOrModified(){
List<ReceiveCommand.Type> types = Arrays.asList(
ReceiveCommand.Type.CREATE, ReceiveCommand.Type.UPDATE, ReceiveCommand.Type.UPDATE_NONFASTFORWARD
List<ReceiveCommand.Type> types = Arrays.asList(
ReceiveCommand.Type.CREATE, ReceiveCommand.Type.UPDATE, ReceiveCommand.Type.UPDATE_NONFASTFORWARD
);
for ( ReceiveCommand.Type type : types ){
checkCreatedOrModified(type);
}
}
private void checkCreatedOrModified(ReceiveCommand.Type type){
GitHookBranchProvider provider = createGitHookBranchProvider(type, "refs/heads/hello");
assertThat(provider.getCreatedOrModified(), Matchers.contains("hello"));
assertThat(provider.getDeletedOrClosed(), empty());
assertThat(provider.getDeletedOrClosed(), empty());
}
/**
* Tests {@link GitHookBranchProvider#getDeletedOrClosed()}.
*/
*/
@Test
public void testGetDeletedOrClosed(){
GitHookBranchProvider provider = createGitHookBranchProvider(ReceiveCommand.Type.DELETE, "refs/heads/hello");
assertThat(provider.getDeletedOrClosed(), Matchers.contains("hello"));
assertThat(provider.getCreatedOrModified(), empty());
}
/**
* Tests {@link GitHookBranchProvider} with a tag instead of a branch.
*/
@@ -98,7 +98,7 @@ public class GitHookBranchProviderTest {
assertThat(provider.getCreatedOrModified(), empty());
assertThat(provider.getDeletedOrClosed(), empty());
}
private GitHookBranchProvider createGitHookBranchProvider(ReceiveCommand.Type type, String refName){
when(command.getType()).thenReturn(type);
when(command.getRefName()).thenReturn(refName);

View File

@@ -0,0 +1,116 @@
/*
* 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.api;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.junit.Test;
import sonia.scm.repository.spi.AbstractGitCommandTestBase;
import java.io.IOException;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class GitHookModificationsProviderTest extends AbstractGitCommandTestBase {
@Test
public void shouldReturnModificationsForNormalUpdate() throws IOException {
GitHookModificationsProvider provider = mockProviderWithChange(ReceiveCommand.Type.UPDATE);
assertThat(provider.getModifications("rename"))
.extracting("modifications")
.asList()
.hasSize(2);
}
@Test
public void shouldReturnModificationsForFastForward() throws IOException {
GitHookModificationsProvider provider = mockProviderWithChange(ReceiveCommand.Type.UPDATE_NONFASTFORWARD);
assertThat(provider.getModifications("rename"))
.extracting("modifications")
.asList()
.hasSize(2);
}
@Test
public void shouldReturnEmptyModificationsForBranchWithRevertedCommit() throws IOException {
GitHookModificationsProvider provider = mockProviderWithChange(ReceiveCommand.Type.UPDATE, "03ca33468c2094249973d0ca11b80243a20de368", "592d797cd36432e591416e8b2b98154f4f163411");
assertThat(provider.getModifications("rename"))
.extracting("modifications")
.asList()
.isEmpty();
}
@Test
public void shouldReturnEmptyModificationsForDeletedBranch() throws IOException {
GitHookModificationsProvider provider = mockProviderWithChange(
ReceiveCommand.Type.DELETE,
"0000000000000000000000000000000000000000",
"fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
assertThat(provider.getModifications("rename"))
.extracting("modifications")
.asList()
.hasSize(5)
.extracting("path")
.contains("a.txt", "b.txt", "c/d.txt", "c/e.txt", "f.txt");
}
@Test
public void shouldReturnEmptyModificationsForCreatedBranch() throws IOException {
GitHookModificationsProvider provider = mockProviderWithChange(
ReceiveCommand.Type.CREATE,
"fcd0ef1831e4002ac43ea539f4094334c79ea9ec",
"0000000000000000000000000000000000000000");
assertThat(provider.getModifications("rename"))
.extracting("modifications")
.asList()
.hasSize(5)
.extracting("path")
.contains("a.txt", "b.txt", "c/d.txt", "c/e.txt", "f.txt");
}
private GitHookModificationsProvider mockProviderWithChange(ReceiveCommand.Type update) throws IOException {
return mockProviderWithChange(
update,
"383b954b27e052db6880d57f1c860dc208795247",
"fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
}
private GitHookModificationsProvider mockProviderWithChange(ReceiveCommand.Type update, String newObjectId, String oldObjectId) throws IOException {
ReceiveCommand receiveCommand = mock(ReceiveCommand.class);
when(receiveCommand.getRefName()).thenReturn("refs/heads/rename");
when(receiveCommand.getType()).thenReturn(update);
when(receiveCommand.getNewId()).thenReturn(ObjectId.fromString(newObjectId));
when(receiveCommand.getOldId()).thenReturn(ObjectId.fromString(oldObjectId));
return new GitHookModificationsProvider(List.of(receiveCommand), createContext().open());
}
}

View File

@@ -43,6 +43,7 @@ import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.api.HookChangesetBuilder;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.HookModificationsProvider;
import java.io.IOException;
import java.util.List;
@@ -164,6 +165,11 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).containsExactly("new_branch");
assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).isEmpty();
HookModificationsProvider modificationsProvider = event.getContext().getModificationsProvider();
assertThat(modificationsProvider.getModifications("new_branch"))
.extracting("modifications")
.asList()
.hasSize(4);
}
@Test
@@ -190,5 +196,10 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
"f360a8738e4a29333786c5817f97a2c912814536",
"d1dfecbfd5b4a2f77fe40e1bde29e640f7f944be"
);
HookModificationsProvider modificationsProvider = event.getContext().getModificationsProvider();
assertThat(modificationsProvider.getModifications("squash"))
.extracting("modifications")
.asList()
.hasSize(8);
}
}

View File

@@ -76,7 +76,7 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
command.getModifications(revision);
Mockito.verify(command.context, times(3)).open();
Mockito.verify(command.context, times(2)).open();
Mockito.verify(repository, never()).close();
}

View File

@@ -21,35 +21,28 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.api;
//~--- non-JDK imports --------------------------------------------------------
package sonia.scm.repository.api;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.spi.HookChangesetProvider;
import sonia.scm.repository.spi.HookChangesetRequest;
import sonia.scm.repository.spi.javahg.AbstractChangesetCommand;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Mercurial hook branch provider implementation.
*
*
* @author Sebastian Sdorra
*/
public class HgHookBranchProvider implements HookBranchProvider
{
private static final Logger logger = LoggerFactory.getLogger(HgHookBranchProvider.class);
private static final HookChangesetRequest REQUEST =
@@ -121,7 +114,7 @@ public class HgHookBranchProvider implements HookBranchProvider
Builder<String> deletedOrClosedBuilder = ImmutableList.builder();
logger.trace("collecting branches from hook changesets");
for (Changeset c : changesets())
{
if (c.getProperty(AbstractChangesetCommand.PROPERTY_CLOSE) != null)

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.api;
import com.google.common.collect.ImmutableList;
@@ -31,7 +31,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Tag;
import sonia.scm.repository.spi.HookChangesetProvider;
import sonia.scm.repository.spi.HookChangesetRequest;
import sonia.scm.repository.spi.HookChangesetResponse;
@@ -44,7 +43,7 @@ import sonia.scm.repository.spi.HookChangesetResponse;
public class HgHookTagProvider implements HookTagProvider {
private static final Logger logger = LoggerFactory.getLogger(HgHookTagProvider.class);
private static final HookChangesetRequest REQUEST = new HookChangesetRequest();
private final HookChangesetProvider changesetProvider;
@@ -77,16 +76,16 @@ public class HgHookTagProvider implements HookTagProvider {
private void collect() {
ImmutableList.Builder<Tag> createdTagsBuilder = ImmutableList.builder();
logger.trace("collecting tags from hook changesets");
HookChangesetResponse response = changesetProvider.handleRequest(REQUEST);
for ( Changeset c : response.getChangesets() ){
appendTags(createdTagsBuilder, c);
}
createdTags = createdTagsBuilder.build();
}
private void appendTags(ImmutableList.Builder<Tag> tags, Changeset c){
List<String> tagNames = c.getTags();
if (tagNames != null){

View File

@@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.HgConfigResolver;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.api.HookChangesetProvider;
import sonia.scm.repository.spi.javahg.HgLogChangesetCommand;
import sonia.scm.web.HgUtil;

View File

@@ -24,8 +24,6 @@
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import sonia.scm.repository.HgConfigResolver;
import sonia.scm.repository.HgRepositoryFactory;
import sonia.scm.repository.Repository;
@@ -33,6 +31,7 @@ import sonia.scm.repository.api.HgHookBranchProvider;
import sonia.scm.repository.api.HgHookMessageProvider;
import sonia.scm.repository.api.HgHookTagProvider;
import sonia.scm.repository.api.HookBranchProvider;
import sonia.scm.repository.api.HookChangesetProvider;
import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookMessageProvider;
import sonia.scm.repository.api.HookTagProvider;

View File

@@ -28,6 +28,7 @@ import com.google.common.collect.ImmutableSet;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.HookBranchProvider;
import sonia.scm.repository.api.HookChangesetProvider;
import sonia.scm.repository.api.HookFeature;
import sonia.scm.repository.api.HookTagProvider;

View File

@@ -33,7 +33,6 @@ import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Tag;
import sonia.scm.repository.spi.HookChangesetProvider;
import sonia.scm.repository.spi.HookChangesetRequest;
import sonia.scm.repository.spi.HookChangesetResponse;

View File

@@ -21,17 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.ImmutableSet;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.RepositoryHookType;
//~--- JDK imports ------------------------------------------------------------
import sonia.scm.repository.api.HookChangesetProvider;
import java.util.Collections;

View File

@@ -37,6 +37,7 @@ import sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.SvnUtil;
import sonia.scm.repository.api.HookChangesetProvider;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.HookFeature;