mirror of
				https://github.com/scm-manager/scm-manager.git
				synced 2025-10-31 18:46:07 +01:00 
			
		
		
		
	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:
		| @@ -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); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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()); | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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)); | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user