diff --git a/src/geotools/src/main/java/org/locationtech/geogig/geotools/cli/porcelain/PGExport.java b/src/geotools/src/main/java/org/locationtech/geogig/geotools/cli/porcelain/PGExport.java index abda8820f2..03549b99a7 100644 --- a/src/geotools/src/main/java/org/locationtech/geogig/geotools/cli/porcelain/PGExport.java +++ b/src/geotools/src/main/java/org/locationtech/geogig/geotools/cli/porcelain/PGExport.java @@ -11,13 +11,17 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import javax.annotation.Nullable; import org.geotools.data.DataStore; +import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.simple.SimpleFeatureStore; +import org.geotools.feature.DefaultFeatureCollection; +import org.geotools.feature.FeatureCollection; import org.geotools.feature.NameImpl; import org.geotools.feature.simple.SimpleFeatureTypeImpl; import org.locationtech.geogig.api.GeoGIG; @@ -32,6 +36,8 @@ import org.locationtech.geogig.api.plumbing.ResolveTreeish; import org.locationtech.geogig.api.plumbing.RevObjectParse; import org.locationtech.geogig.api.plumbing.RevParse; +import org.locationtech.geogig.api.plumbing.diff.DiffEntry; +import org.locationtech.geogig.api.porcelain.DiffOp; import org.locationtech.geogig.cli.CLICommand; import org.locationtech.geogig.cli.CommandFailedException; import org.locationtech.geogig.cli.GeogigCLI; @@ -39,6 +45,7 @@ import org.locationtech.geogig.cli.annotation.ReadOnly; import org.locationtech.geogig.geotools.plumbing.ExportOp; import org.locationtech.geogig.geotools.plumbing.GeoToolsOpException; +import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.filter.Filter; @@ -71,6 +78,12 @@ public class PGExport extends AbstractPGCommand implements CLICommand { @Nullable public String sFeatureTypeId; + @Parameter(names = { "--since" }, description = "Only export changes that happend after the given commit") + public String since; + + @Parameter(names = { "--until" }, description = "Only export changes until the given commit") + public String until; + /** * Executes the export command using the provided options. */ @@ -86,6 +99,7 @@ protected void runInternal(GeogigCLI cli) throws IOException { String tableName = args.get(1); checkParameter(tableName != null && !tableName.isEmpty(), "No table name specified"); + checkParameter(!overwrite || ((since == null && until == null)), "--overwrite can not be specified together with --since/--until"); DataStore dataStore = getDataStore(); @@ -122,7 +136,7 @@ protected void runInternal(GeogigCLI cli) throws IOException { throw new CommandFailedException("Cannot create new table in database", e); } } else { - if (!overwrite) { + if (!overwrite && since == null) { throw new InvalidParameterException( "The selected table already exists. Use -o to overwrite"); } @@ -143,7 +157,7 @@ protected void runInternal(GeogigCLI cli) throws IOException { throw new CommandFailedException("Error trying to remove features", e); } } - ExportOp op = cli.getGeogig().command(ExportOp.class).setFeatureStore(featureStore) + ExportOp op = cli.getGeogig().command(ExportOp.class).setOldRef(since).setNewRef(until).setFeatureStore(featureStore) .setPath(path).setFilterFeatureTypeId(featureTypeId).setAlter(alter); if (defaultType) { op.exportDefaultFeatureType(); diff --git a/src/geotools/src/main/java/org/locationtech/geogig/geotools/plumbing/ExportOp.java b/src/geotools/src/main/java/org/locationtech/geogig/geotools/plumbing/ExportOp.java index c84ef9fa66..a4b954dd52 100644 --- a/src/geotools/src/main/java/org/locationtech/geogig/geotools/plumbing/ExportOp.java +++ b/src/geotools/src/main/java/org/locationtech/geogig/geotools/plumbing/ExportOp.java @@ -9,59 +9,45 @@ */ package org.locationtech.geogig.geotools.plumbing; -import static com.google.common.base.Preconditions.checkArgument; - -import java.io.IOException; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.annotation.Nullable; - +import com.google.common.base.*; +import com.google.common.base.Optional; +import com.google.common.collect.*; import org.geotools.data.DefaultTransaction; import org.geotools.data.Transaction; +import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureStore; +import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.Hints; +import org.geotools.feature.DefaultFeatureCollection; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.feature.collection.BaseFeatureCollection; import org.geotools.feature.collection.DelegateFeatureIterator; -import org.locationtech.geogig.api.AbstractGeoGigOp; -import org.locationtech.geogig.api.FeatureBuilder; -import org.locationtech.geogig.api.NodeRef; -import org.locationtech.geogig.api.ObjectId; -import org.locationtech.geogig.api.ProgressListener; -import org.locationtech.geogig.api.RevFeature; -import org.locationtech.geogig.api.RevFeatureImpl; -import org.locationtech.geogig.api.RevFeatureType; -import org.locationtech.geogig.api.RevObject; +import org.locationtech.geogig.api.*; import org.locationtech.geogig.api.RevObject.TYPE; -import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.api.hooks.Hookable; import org.locationtech.geogig.api.plumbing.FindTreeChild; import org.locationtech.geogig.api.plumbing.ResolveTreeish; import org.locationtech.geogig.api.plumbing.diff.DepthTreeIterator; import org.locationtech.geogig.api.plumbing.diff.DepthTreeIterator.Strategy; +import org.locationtech.geogig.api.plumbing.diff.DiffEntry; +import org.locationtech.geogig.api.porcelain.DiffOp; import org.locationtech.geogig.geotools.plumbing.GeoToolsOpException.StatusCode; import org.locationtech.geogig.storage.ObjectDatabase; import org.opengis.feature.Feature; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.PropertyDescriptor; +import org.opengis.filter.Filter; +import org.opengis.filter.FilterFactory; +import org.opengis.filter.identity.Identifier; -import com.google.common.base.Function; -import com.google.common.base.Optional; -import com.google.common.base.Predicate; -import com.google.common.base.Predicates; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; -import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterators; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.UnmodifiableIterator; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.google.common.base.Preconditions.checkArgument; /** * Internal operation for creating a FeatureCollection from a tree content. @@ -94,6 +80,10 @@ public Optional apply(@Nullable Feature feature) { private boolean transactional; + private String oldRef; + + private String newRef; + /** * Constructs a new export operation. */ @@ -114,7 +104,8 @@ protected SimpleFeatureStore _call() { checkArgument(filterType instanceof RevFeatureType, "Provided filter feature type is does not exist"); } - + checkArgument((oldRef == null) == (newRef == null), "--since and --until must both be set or none of them"); + boolean isChangeExport = (oldRef != null || newRef != null); final SimpleFeatureStore targetStore = getTargetStore(); final String refspec = resolveRefSpec(); @@ -131,33 +122,11 @@ protected SimpleFeatureStore _call() { progressListener.started(); progressListener.setDescription("Exporting from " + path + " to " + targetStore.getName().getLocalPart() + "... "); - - FeatureCollection asFeatureCollection = new BaseFeatureCollection() { - - @Override - public FeatureIterator features() { - - final Iterator plainFeatures = getFeatures(typeTree, database, - defaultMetadataId, progressListener); - - Iterator adaptedFeatures = adaptToArguments(plainFeatures, - defaultMetadataId); - - Iterator> transformed = Iterators.transform(adaptedFeatures, - ExportOp.this.function); - - Iterator filtered = Iterators.filter(Iterators.transform( - transformed, new Function, SimpleFeature>() { - @Override - public SimpleFeature apply(Optional input) { - return (SimpleFeature) (input.isPresent() ? input.get() : null); - } - }), Predicates.notNull()); - - return new DelegateFeatureIterator(filtered); - } - }; - + FeatureCollection asFeatureCollection = null; + if(!isChangeExport) { + final Iterator plainFeatures = getFeatures(typeTree, database, defaultMetadataId, progressListener); + asFeatureCollection = getFeatureCollection(defaultMetadataId, plainFeatures); + } // add the feature collection to the feature store final Transaction transaction; if (transactional) { @@ -168,7 +137,11 @@ public SimpleFeature apply(Optional input) { try { targetStore.setTransaction(transaction); try { - targetStore.addFeatures(asFeatureCollection); + if(isChangeExport) { + exportChanges(database, defaultMetadataId, targetStore, progressListener); + } else { + targetStore.addFeatures(asFeatureCollection); + } transaction.commit(); } catch (final Exception e) { if (transactional) { @@ -182,33 +155,80 @@ public SimpleFeature apply(Optional input) { } catch (IOException e) { throw new GeoToolsOpException(e, StatusCode.UNABLE_TO_ADD); } - progressListener.complete(); return targetStore; } - private static Iterator getFeatures(final RevTree typeTree, - final ObjectDatabase database, final ObjectId defaultMetadataId, - final ProgressListener progressListener) { + private FeatureCollection + getFeatureCollection(final ObjectId defaultMetadataId, final Iterator plainFeatures) { + return new BaseFeatureCollection() { - Iterator nodes = new DepthTreeIterator("", defaultMetadataId, typeTree, database, - Strategy.FEATURES_ONLY); + @Override + public FeatureIterator features() { - // progress reporting - nodes = Iterators.transform(nodes, new Function() { + Iterator adaptedFeatures = adaptToArguments(plainFeatures, + defaultMetadataId); - private AtomicInteger count = new AtomicInteger(); + Iterator> transformed = Iterators.transform(adaptedFeatures, + ExportOp.this.function); + + Iterator filtered = Iterators.filter(Iterators.transform( + transformed, new Function, SimpleFeature>() { + @Override + public SimpleFeature apply(Optional input) { + return (SimpleFeature) (input.isPresent() ? input.get() : null); + } + }), Predicates.notNull()); + + return new DelegateFeatureIterator(filtered); + } + }; + } + + private static Filter createFeatureFilter(List ids) { + FilterFactory ff = CommonFactoryFinder.getFilterFactory2(); + Set fids = new HashSet<>(); + for(String id : ids) { + fids.add(ff.featureId(id)); + } + return ff.id(fids); + } + private SimpleFeatureStore exportChanges(ObjectDatabase database, final ObjectId defaultMetadataId, SimpleFeatureStore targetStore, final ProgressListener progressListener) throws IOException{ + Function node2feat = convertFunction(database); + Iterator it = command(DiffOp.class).setOldVersion(oldRef).setNewVersion(newRef).setFilter(path).call(); + progressListener.setProgress(20); + final List toAdd = new ArrayList(); + List toRemove = new ArrayList(); + while(it.hasNext()) { + final DiffEntry entry = it.next(); + if(entry.isDelete() || entry.isChange()) { + toRemove.add(entry.getOldObject().name()); + } + if(entry.isAdd() || entry.isChange()) { + toAdd.add(node2feat.apply(entry.getNewObject())); + } + } + targetStore.removeFeatures(createFeatureFilter(toRemove)); + progressListener.setProgress(60); + Function progressFunction = new Function() { + AtomicInteger count = new AtomicInteger(); + @Nullable @Override - public NodeRef apply(NodeRef input) { - progressListener.setProgress((count.incrementAndGet() * 100.f) / typeTree.size()); + public SimpleFeature apply(@Nullable SimpleFeature input) { + progressListener.setProgress(60.0f + (count.incrementAndGet() * 40.0f) / toAdd.size()); return input; } - }); + }; + targetStore.addFeatures(getFeatureCollection(defaultMetadataId, + Iterators.filter(Iterators.transform(toAdd.iterator(), progressFunction), Predicates.notNull()))); + return targetStore; + } - Function asFeature = new Function() { + private static Function convertFunction(final ObjectDatabase database) { + return new Function() { private Map ftCache = Maps.newHashMap(); @@ -240,6 +260,28 @@ private FeatureBuilder getBuilderFor(final ObjectId metadataId) { return featureBuilder; } }; + } + + private static Iterator getFeatures(final RevTree typeTree, + final ObjectDatabase database, final ObjectId defaultMetadataId, + final ProgressListener progressListener) { + + Iterator nodes = new DepthTreeIterator("", defaultMetadataId, typeTree, database, + Strategy.FEATURES_ONLY); + + // progress reporting + nodes = Iterators.transform(nodes, new Function() { + + private AtomicInteger count = new AtomicInteger(); + + @Override + public NodeRef apply(NodeRef input) { + progressListener.setProgress((count.incrementAndGet() * 100.f) / typeTree.size()); + return input; + } + }); + + Function asFeature = convertFunction(database); Iterator asFeatures = Iterators.transform(nodes, asFeature); @@ -496,4 +538,14 @@ public ExportOp setTransactional(boolean transactional) { this.transactional = transactional; return this; } + + public ExportOp setOldRef(String oldRef) { + this.oldRef = oldRef; + return this; + } + + public ExportOp setNewRef(String newRef) { + this.newRef = newRef; + return this; + } } diff --git a/src/geotools/src/test/java/org/locationtech/geogig/geotools/cli/porcelain/PGExportTest.java b/src/geotools/src/test/java/org/locationtech/geogig/geotools/cli/porcelain/PGExportTest.java index bb159da61e..ac712a0cda 100644 --- a/src/geotools/src/test/java/org/locationtech/geogig/geotools/cli/porcelain/PGExportTest.java +++ b/src/geotools/src/test/java/org/locationtech/geogig/geotools/cli/porcelain/PGExportTest.java @@ -17,6 +17,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.locationtech.geogig.api.RevCommit; import org.locationtech.geogig.api.porcelain.CommitOp; import org.locationtech.geogig.cli.CommandFailedException; import org.locationtech.geogig.cli.GeogigCLI; @@ -30,6 +31,8 @@ public class PGExportTest extends RepositoryTestCase { private GeogigCLI cli; + private RevCommit head; + @Override public void setUpInternal() throws Exception { ConsoleReader consoleReader = new ConsoleReader(System.in, System.out, @@ -50,7 +53,7 @@ public void setUpInternal() throws Exception { insertAndAdd(lines2); insertAndAdd(lines3); - geogig.command(CommitOp.class).call(); + head = geogig.command(CommitOp.class).call(); } @Override @@ -159,4 +162,16 @@ public void testExportWithFeatureNameInsteadOfType() throws Exception { exception.expect(InvalidParameterException.class); exportCommand.run(cli); } + + @Test + public void testChangeExport() throws Exception { + PGExport exportCommand = new PGExport(); + insertAndAdd(points1_modified); + RevCommit until = geogig.command(CommitOp.class).call(); + exportCommand.until = until.getTreeId().toString(); + exportCommand.since = head.getTreeId().toString(); + exportCommand.args = Arrays.asList("Points", "Points"); + exportCommand.dataStoreFactory = TestHelper.createTestFactory(); + exportCommand.runInternal(cli); + } } diff --git a/src/geotools/src/test/java/org/locationtech/geogig/geotools/plumbing/ExportOpTest.java b/src/geotools/src/test/java/org/locationtech/geogig/geotools/plumbing/ExportOpTest.java index 87ea12a2b2..52b5b32935 100644 --- a/src/geotools/src/test/java/org/locationtech/geogig/geotools/plumbing/ExportOpTest.java +++ b/src/geotools/src/test/java/org/locationtech/geogig/geotools/plumbing/ExportOpTest.java @@ -9,22 +9,33 @@ */ package org.locationtech.geogig.geotools.plumbing; -import java.util.List; +import java.io.IOException; +import java.util.*; import javax.annotation.Nullable; +import com.google.common.collect.Sets; import org.geotools.data.DataUtilities; +import org.geotools.data.FeatureWriter; +import org.geotools.data.Query; +import org.geotools.data.Transaction; import org.geotools.data.memory.MemoryDataStore; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.data.simple.SimpleFeatureStore; +import org.geotools.factory.CommonFactoryFinder; +import org.geotools.factory.FactoryFinder; +import org.geotools.factory.GeoTools; +import org.geotools.factory.Hints; +import org.geotools.feature.DefaultFeatureCollection; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.junit.Test; -import org.locationtech.geogig.api.ObjectId; -import org.locationtech.geogig.api.RevFeatureTypeImpl; +import org.locationtech.geogig.api.*; import org.locationtech.geogig.api.porcelain.AddOp; import org.locationtech.geogig.api.porcelain.CommitOp; +import org.locationtech.geogig.cli.porcelain.Commit; +import org.locationtech.geogig.geotools.cli.porcelain.PGExport; import org.locationtech.geogig.geotools.plumbing.GeoToolsOpException.StatusCode; import org.locationtech.geogig.test.integration.RepositoryTestCase; import org.opengis.feature.Feature; @@ -33,6 +44,10 @@ import com.google.common.base.Function; import com.google.common.base.Optional; +import org.opengis.filter.Filter; +import org.opengis.filter.FilterFactory; +import org.opengis.filter.identity.Identifier; +import sun.java2d.pipe.SpanShapeRenderer; public class ExportOpTest extends RepositoryTestCase { @@ -328,4 +343,126 @@ public void testExportFromTreeWithSeveralFeatureTypes() throws Exception { } } + private static MemoryDataStore getChangeStore(SimpleFeatureType type) { + return new MemoryDataStore(type) { + @Override + protected Set getSupportedHints() { + return Sets.newHashSet(Hints.USE_PROVIDED_FID); + } + }; + } + + @Test + public void exportChangeAdd() throws Exception { + insertAndAdd(points1); + RevCommit commit1 = geogig.command(CommitOp.class).setMessage("Some message").call(); + MemoryDataStore store = getChangeStore(pointsType); + final String typeName = store.getTypeNames()[0]; + SimpleFeatureSource featureSource = store.getFeatureSource(typeName); + SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; + geogig.command(ExportOp.class).setFeatureStore(featureStore).setPath(pointsName).setTransactional(false).call(); + SimpleFeatureCollection collection1 = featureStore.getFeatures(); + assertEquals(1, collection1.size()); + assertTrue(collectionsAreEqual(collection1.features(), new Feature[]{points1})); + insertAndAdd(points2); + RevCommit commit2 = geogig.command(CommitOp.class).setMessage("Some message").call(); + insertAndAdd(points3); + RevCommit commit3 = geogig.command(CommitOp.class).setMessage("Some message").call(); + geogig.command(ExportOp.class).setFeatureStore(featureStore).setOldRef(commit1.getTreeId().toString()) + .setNewRef(commit3.getTreeId().toString()).setPath(pointsName).setTransactional(false).call(); + SimpleFeatureCollection collection2 = featureStore.getFeatures(); + assertEquals(3, collection2.size()); + assertTrue(collectionsAreEqual(collection2.features(), new Feature[]{points1, points2, points3})); + } + + @Test + public void exportChangeModify() throws Exception { + insertAndAdd(points1); + RevCommit commit1 = geogig.command(CommitOp.class).setMessage("Some message").call(); + MemoryDataStore store = getChangeStore(pointsType); + final String typeName = store.getTypeNames()[0]; + SimpleFeatureSource featureSource = store.getFeatureSource(typeName); + SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; + geogig.command(ExportOp.class).setFeatureStore(featureStore).setPath(pointsName).setTransactional(false).call(); + SimpleFeatureCollection collection1 = featureStore.getFeatures(); + assertEquals(1, collection1.size()); + assertTrue(collectionsAreEqual(collection1.features(), new Feature[]{points1})); + insertAndAdd(points1_modified); + RevCommit commit2 = geogig.command(CommitOp.class).setMessage("Some message").call(); + geogig.command(ExportOp.class).setFeatureStore(featureStore).setOldRef(commit1.getTreeId().toString()) + .setNewRef(commit2.getTreeId().toString()).setPath(pointsName).setTransactional(false).call(); + SimpleFeatureCollection collection2 = featureStore.getFeatures(); + assertEquals(1, collection2.size()); + assertTrue(collectionsAreEqual(collection2.features(), new Feature[]{points1_modified})); + } + + @Test + public void exportChangeDelete() throws Exception { + insertAndAdd(points1); + RevCommit commit1 = geogig.command(CommitOp.class).setMessage("Some message").call(); + MemoryDataStore store = getChangeStore(pointsType); + final String typeName = store.getTypeNames()[0]; + SimpleFeatureSource featureSource = store.getFeatureSource(typeName); + SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; + geogig.command(ExportOp.class).setFeatureStore(featureStore).setPath(pointsName).setTransactional(false).call(); + SimpleFeatureCollection collection1 = featureStore.getFeatures(); + assertEquals(1, collection1.size()); + assertTrue(collectionsAreEqual(collection1.features(), new Feature[]{points1})); + deleteAndAdd(points1); + RevCommit commit2 = geogig.command(CommitOp.class).setMessage("Some message").call(); + geogig.command(ExportOp.class).setFeatureStore(featureStore).setOldRef(commit1.getTreeId().toString()) + .setNewRef(commit2.getTreeId().toString()).setPath(pointsName).setTransactional(false).call(); + SimpleFeatureCollection collection2 = featureStore.getFeatures(); + assertEquals(0, collection2.size()); + } + + private void testChangeExport(Feature[] expected, RevCommit since, RevCommit until, SimpleFeatureStore featureStore) throws Exception { + geogig.command(ExportOp.class).setFeatureStore(featureStore).setOldRef(since.getTreeId().toString()) + .setNewRef(until.getTreeId().toString()).setPath(pointsName).setTransactional(false).call(); + SimpleFeatureCollection collection = featureStore.getFeatures(); + assertEquals(expected.length, collection.size()); + assertTrue(collectionsAreEqual(collection.features(), expected)); + } + + @Test + public void exportChangeMultipleDelete() throws Exception { + insertAndAdd(points1); + RevCommit commit1 = geogig.command(CommitOp.class).setMessage("Some message").call(); + MemoryDataStore store = getChangeStore(pointsType); + final String typeName = store.getTypeNames()[0]; + SimpleFeatureSource featureSource = store.getFeatureSource(typeName); + SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; + geogig.command(ExportOp.class).setFeatureStore(featureStore).setPath(pointsName).setTransactional(false).call(); + SimpleFeatureCollection collection1 = featureStore.getFeatures(); + assertEquals(1, collection1.size()); + assertTrue(collectionsAreEqual(collection1.features(), new Feature[]{points1})); + deleteAndAdd(points1); + RevCommit commit2 = geogig.command(CommitOp.class).setMessage("Some message").call(); + insertAndAdd(points1); + RevCommit commit3 = geogig.command(CommitOp.class).setMessage("Some message").call(); + deleteAndAdd(points1); + RevCommit commit4 = geogig.command(CommitOp.class).setMessage("Some message").call(); + testChangeExport(new Feature[] {}, commit1, commit2, featureStore); + testChangeExport(new Feature[] {points1}, commit2, commit3, featureStore); + testChangeExport(new Feature[] {}, commit3, commit4, featureStore); + } + + @Test + public void testChangeMultiple() throws Exception { + insertAndAdd(points1); + RevCommit commit1 = geogig.command(CommitOp.class).setMessage("Some message").call(); + MemoryDataStore store = getChangeStore(pointsType); + final String typeName = store.getTypeNames()[0]; + SimpleFeatureSource featureSource = store.getFeatureSource(typeName); + SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; + geogig.command(ExportOp.class).setFeatureStore(featureStore).setPath(pointsName).setTransactional(false).call(); + SimpleFeatureCollection collection1 = featureStore.getFeatures(); + assertEquals(1, collection1.size()); + assertTrue(collectionsAreEqual(collection1.features(), new Feature[]{points1})); + insertAndAdd(points2); + RevCommit commit2 = geogig.command(CommitOp.class).setMessage("Some message").call(); + insertAndAdd(points1_modified); + RevCommit commit3 = geogig.command(CommitOp.class).setMessage("Some message").call(); + testChangeExport(new Feature[] {points1_modified, points2}, commit1, commit3, featureStore); + } }