Modifications command between two revisions (#1761)

Adds the option to compute the modifications between two revisions unsing the modifications command.
This commit is contained in:
René Pfeuffer
2021-08-09 12:13:41 +02:00
committed by GitHub
parent ddd2fc1055
commit 8558572c99
22 changed files with 696 additions and 100 deletions

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;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableSet;
/**
* This class can be used to "consolidate" modifications for a stream of commit based modifications.
* Single modifications will be changed or ignored if subsequent modifications effect them. An "added"
* for a file for example will be ignored, if the same file is marked as "removed" later on. Another
* example would be a "modification" of a file, that was "added" beforehand, because in summary this
* simply is still an "added" file.
*/
class ConsolidatingModificationCollector implements Collector<Modification, ConsolidatedModifications, Collection<Modification>> {
static ConsolidatingModificationCollector consolidate() {
return new ConsolidatingModificationCollector();
}
@Override
public Supplier<ConsolidatedModifications> supplier() {
return ConsolidatedModifications::new;
}
@Override
public BiConsumer<ConsolidatedModifications, Modification> accumulator() {
return (existingModifications, currentModification) -> {
if (currentModification instanceof Copied) {
existingModifications.added(new Added(((Copied) currentModification).getTargetPath()));
} else if (currentModification instanceof Removed) {
existingModifications.removed((Removed) currentModification);
} else if (currentModification instanceof Renamed) {
Renamed renameModification = (Renamed) currentModification;
existingModifications.removed(new Removed(renameModification.getOldPath()));
existingModifications.added(new Added(renameModification.getNewPath()));
} else if (currentModification instanceof Added) {
existingModifications.added((Added) currentModification);
} else if (currentModification instanceof Modified) {
existingModifications.modified((Modified) currentModification);
} else {
throw new IllegalStateException("cannot handle modification of unknown type " + currentModification.getClass());
}
};
}
@Override
public BinaryOperator<ConsolidatedModifications> combiner() {
return null; // Combiner not needed because we do not support Collector.Characteristics#CONCURRENT
}
@Override
public Function<ConsolidatedModifications, Collection<Modification>> finisher() {
return ConsolidatedModifications::getModifications;
}
@Override
public Set<Characteristics> characteristics() {
return emptySet();
}
}
class ConsolidatedModifications {
private final Map<String, Modification> modifications = new HashMap<>();
Set<String> getPaths() {
return unmodifiableSet(modifications.keySet());
}
Collection<Modification> getModifications() {
return unmodifiableCollection(modifications.values());
}
void added(Added added) {
Modification earlierModification = modifications.get(added.getPath());
if (earlierModification instanceof Removed) {
modifications.put(added.getPath(), new Modified(added.getPath()));
} else {
modifications.put(added.getPath(), added);
}
}
void modified(Modified modified) {
Modification earlierModification = modifications.get(modified.getPath());
if (!(earlierModification instanceof Added)) { // added should still be added
modified.getEffectedPaths().forEach(path -> modifications.put(path, modified));
}
}
void removed(Removed removed) {
Modification earlierModification = modifications.get(removed.getPath());
if (earlierModification instanceof Added) {
modifications.remove(removed.getPath());
} else {
modifications.put(removed.getPath(), removed);
}
}
}

View File

@@ -47,15 +47,17 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.empty;
import static java.util.stream.Collectors.toList;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
import static sonia.scm.repository.ConsolidatingModificationCollector.consolidate;
//~--- JDK imports ------------------------------------------------------------
@@ -118,22 +120,29 @@ public final class SvnUtil
return result;
}
public static Modifications createModifications(String startRevision, String endRevision, Collection<SVNLogEntry> entries) {
Collection<Modification> consolidatedModifications =
entries.stream()
.flatMap(SvnUtil::createModificationStream)
.collect(consolidate());
return new Modifications(startRevision, endRevision, consolidatedModifications);
}
public static Modifications createModifications(SVNLogEntry entry, String revision) {
return new Modifications(revision, createModificationStream(entry).collect(toList()));
}
private static Stream<Modification> createModificationStream(SVNLogEntry entry) {
Map<String, SVNLogEntryPath> changeMap = entry.getChangedPaths();
List<Modification> modificationList;
if (Util.isNotEmpty(changeMap)) {
modificationList = changeMap.values().stream()
return changeMap.values().stream()
.map(e -> asModification(e.getType(), e.getPath()))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
.map(Optional::get);
} else {
modificationList = emptyList();
return Stream.empty();
}
return new Modifications(revision, modificationList);
}
public static Optional<Modification> asModification(char type, String path) {
@@ -383,4 +392,5 @@ public final class SvnUtil
{
return Strings.nullToEmpty(id).startsWith(ID_TRANSACTION_PREFIX);
}
}

View File

@@ -48,14 +48,12 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif
@Override
public Modifications getModifications(String revisionOrTransactionId) {
Modifications modifications;
try {
if (SvnUtil.isTransactionEntryId(revisionOrTransactionId)) {
modifications = getModificationsFromTransaction(SvnUtil.getTransactionId(revisionOrTransactionId));
return getModificationsFromTransaction(SvnUtil.getTransactionId(revisionOrTransactionId));
} else {
modifications = getModificationFromRevision(revisionOrTransactionId);
return getModificationFromRevision(revisionOrTransactionId, revisionOrTransactionId);
}
return modifications;
} catch (SVNException ex) {
throw new InternalRepositoryException(
repository,
@@ -65,15 +63,33 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif
}
}
@Override
public Modifications getModifications(String baseRevision, String revision) {
try {
return getModificationFromRevision(baseRevision, revision);
} catch (SVNException ex) {
throw new InternalRepositoryException(
repository,
"failed to get svn modifications from " + baseRevision + " to " + revision,
ex
);
}
}
@SuppressWarnings("unchecked")
private Modifications getModificationFromRevision(String revision) throws SVNException {
log.debug("get svn modifications from revision: {}", revision);
long revisionNumber = SvnUtil.getRevisionNumber(revision, repository);
private Modifications getModificationFromRevision(String startRevision, String endRevision) throws SVNException {
log.debug("get svn modifications from revision {} to {}", startRevision, endRevision);
long startRevisionNumber = SvnUtil.getRevisionNumber(startRevision, repository);
long endRevisionNumber = SvnUtil.getRevisionNumber(endRevision, repository);
SVNRepository repo = open();
Collection<SVNLogEntry> entries = repo.log(null, null, revisionNumber,
revisionNumber, true, true);
Collection<SVNLogEntry> entries = repo.log(null, null, startRevisionNumber,
endRevisionNumber, true, true);
if (Util.isNotEmpty(entries)) {
return SvnUtil.createModifications(entries.iterator().next(), revision);
if (startRevision.equals(endRevision)) {
return SvnUtil.createModifications(entries.iterator().next(), endRevision);
} else {
return SvnUtil.createModifications(startRevision, endRevision, entries);
}
}
return null;
}
@@ -87,10 +103,4 @@ public class SvnModificationsCommand extends AbstractSvnCommand implements Modif
return new Modifications(null, modificationList);
}
@Override
public Modifications getModifications(ModificationsCommandRequest request) {
return getModifications(request.getRevision());
}
}

View File

@@ -26,6 +26,7 @@ package sonia.scm.repository.spi;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Closeables;
import sonia.scm.repository.Feature;
import sonia.scm.repository.Repository;
import sonia.scm.repository.SvnRepositoryHandler;
import sonia.scm.repository.SvnWorkingCopyFactory;
@@ -34,6 +35,7 @@ import sonia.scm.repository.api.HookContextFactory;
import javax.net.ssl.TrustManager;
import java.io.IOException;
import java.util.EnumSet;
import java.util.Set;
/**
@@ -41,7 +43,6 @@ import java.util.Set;
*/
public class SvnRepositoryServiceProvider extends RepositoryServiceProvider {
//J-
public static final Set<Command> COMMANDS = ImmutableSet.of(
Command.BLAME,
Command.BROWSE,
@@ -55,8 +56,10 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider {
Command.FULL_HEALTH_CHECK,
Command.MIRROR
);
//J+
public static final Set<Feature> FEATURES = EnumSet.of(
Feature.MODIFICATIONS_BETWEEN_REVISIONS
);
private final SvnContext context;
private final SvnWorkingCopyFactory workingCopyFactory;
@@ -129,6 +132,11 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider {
return COMMANDS;
}
@Override
public Set<Feature> getSupportedFeatures() {
return FEATURES;
}
@Override
public UnbundleCommand getUnbundleCommand() {
return new SvnUnbundleCommand(context, hookContextFactory, new SvnLogCommand(context));

View File

@@ -0,0 +1,133 @@
/*
* 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;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collection;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class ConsolidatingModificationCollectorTest {
@Test
void shouldKeepIndependentChanges() {
Collection<Modification> consolidated =
Stream.of(
new Added("added"),
new Removed("removed"),
new Modified("modified")
).collect(new ConsolidatingModificationCollector());
assertThat(consolidated)
.extracting("class")
.containsExactlyInAnyOrder(Added.class, Removed.class, Modified.class);
}
@Test
void shouldNotListAddedFileIfRemovedLaterOn() {
Collection<Modification> consolidated =
Stream.of(
new Added("file"),
new Removed("file")
).collect(new ConsolidatingModificationCollector());
assertThat(consolidated)
.isEmpty();
}
@Test
void shouldReplaceModificationWithRemove() {
Collection<Modification> consolidated =
Stream.of(
new Modified("file"),
new Removed("file")
).collect(new ConsolidatingModificationCollector());
assertThat(consolidated)
.extracting("class")
.containsExactly(Removed.class);
}
@Test
void shouldReplaceCopyWithAdd() {
Collection<Modification> consolidated =
Stream.of(
new Copied("source", "target")
).collect(new ConsolidatingModificationCollector());
assertThat(consolidated)
.extracting("class")
.containsExactly(Added.class);
assertThat(consolidated)
.extracting("path")
.containsExactly("target");
}
@Test
void shouldReplaceRenameWithAddAndRemove() {
Collection<Modification> consolidated =
Stream.of(
new Renamed("source", "target")
).collect(new ConsolidatingModificationCollector());
assertThat(consolidated)
.extracting("class")
.containsExactlyInAnyOrder(Added.class, Removed.class);
assertThat(consolidated)
.extracting("path")
.containsExactlyInAnyOrder("source", "target");
}
@Test
void shouldNotReplaceAddWithModify() {
Collection<Modification> consolidated =
Stream.of(
new Added("file"),
new Modified("file")
).collect(new ConsolidatingModificationCollector());
assertThat(consolidated)
.extracting("class")
.containsExactlyInAnyOrder(Added.class);
}
@Test
void shouldReplaceAddWithModifyIfRemovedBefore() {
Collection<Modification> consolidated =
Stream.of(
new Removed("file"),
new Added("file")
).collect(new ConsolidatingModificationCollector());
assertThat(consolidated)
.extracting("class")
.containsExactlyInAnyOrder(Modified.class);
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 sonia.scm.repository.Modifications;
import static org.assertj.core.api.Assertions.assertThat;
public class SvnModificationsCommandTest extends AbstractSvnCommandTestBase {
@Test
public void shouldReadModificationsForSingleRevision() {
SvnContext context = createContext();
SvnModificationsCommand svnModificationsCommand = new SvnModificationsCommand(context);
Modifications modifications = svnModificationsCommand.getModifications("4");
assertThat(modifications.getAdded()).hasSize(3);
}
@Test
public void shouldReadModificationsForMultipleRevisions() {
SvnContext context = createContext();
SvnModificationsCommand svnModificationsCommand = new SvnModificationsCommand(context);
Modifications modifications = svnModificationsCommand.getModifications("1", "4");
assertThat(modifications.getModifications()).hasSize(4);
}
}