Browse Source

Preliminary support for tree:<depth> filter

This is used when fetching, and in particular to populate a partial
clone or a virtual file system cache as the user navigates. With this,
a client can pre-fetch a few directories deeper than only the current
directory.

depth:0 will omit all trees, and is useful if you only want to fetch
the commits of a repository, or fetch just a single tree or blob object.
depth:1 will fetch only the root tree of all commits fetched. depth:2
will fetch the root tree and all blobs and tree objects directly
referenced from it. depth:3 gets one more level, and so on. depth:#
will not filter a blob or tree that is explicitly marked wanted.

Bitmaps are disabled when this filter is used.

This implementation is quite slow because it iterates over all omitted
objects rather than skipping them. This will be addressed in follow-up
commits.

Change-Id: Ic312fee22d60e32cfcad59da56980e90ae2cae6a
Signed-off-by: Matthew DeVore <matvore@gmail.com>
stable-5.4
Matthew DeVore 6 years ago
parent
commit
93dd2d482a
  1. 1
      org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV0ParserTest.java
  2. 14
      org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV2ParserTest.java
  3. 200
      org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
  4. 54
      org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
  5. 107
      org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
  6. 80
      org.eclipse.jgit/src/org/eclipse/jgit/transport/FilterSpec.java
  7. 3
      org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java

1
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV0ParserTest.java

@ -194,6 +194,7 @@ public class ProtocolV0ParserTest {
hasOnlyObjectIds("4624442d68ee402a94364191085b77137618633e", hasOnlyObjectIds("4624442d68ee402a94364191085b77137618633e",
"f900c8326a43303685c46b279b9f70411bff1a4b")); "f900c8326a43303685c46b279b9f70411bff1a4b"));
assertEquals(13000, request.getFilterSpec().getBlobLimit()); assertEquals(13000, request.getFilterSpec().getBlobLimit());
assertEquals(-1, request.getFilterSpec().getTreeDepthLimit());
} }
} }

14
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV2ParserTest.java

@ -233,6 +233,7 @@ public class ProtocolV2ParserTest {
ConfigBuilder.start().allowFilter().done()); ConfigBuilder.start().allowFilter().done());
FetchV2Request request = parser.parseFetchRequest(pckIn); FetchV2Request request = parser.parseFetchRequest(pckIn);
assertEquals(0, request.getFilterSpec().getBlobLimit()); assertEquals(0, request.getFilterSpec().getBlobLimit());
assertEquals(-1, request.getFilterSpec().getTreeDepthLimit());
} }
@Test @Test
@ -244,6 +245,19 @@ public class ProtocolV2ParserTest {
ConfigBuilder.start().allowFilter().done()); ConfigBuilder.start().allowFilter().done());
FetchV2Request request = parser.parseFetchRequest(pckIn); FetchV2Request request = parser.parseFetchRequest(pckIn);
assertEquals(15, request.getFilterSpec().getBlobLimit()); assertEquals(15, request.getFilterSpec().getBlobLimit());
assertEquals(-1, request.getFilterSpec().getTreeDepthLimit());
}
@Test
public void testFetchWithTreeDepthFilter() throws IOException {
PacketLineIn pckIn = formatAsPacketLine(PacketLineIn.DELIM,
"filter tree:3",
PacketLineIn.END);
ProtocolV2Parser parser = new ProtocolV2Parser(
ConfigBuilder.start().allowFilter().done());
FetchV2Request request = parser.parseFetchRequest(pckIn);
assertEquals(-1, request.getFilterSpec().getBlobLimit());
assertEquals(3, request.getFilterSpec().getTreeDepthLimit());
} }
@Test @Test

200
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java

@ -16,11 +16,16 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.errors.PackProtocolException;
import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.storage.dfs.DfsGarbageCollector; import org.eclipse.jgit.internal.storage.dfs.DfsGarbageCollector;
@ -28,6 +33,8 @@ import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
@ -1551,6 +1558,199 @@ public class UploadPackTest {
assertTrue(client.getObjectDatabase().has(small.toObjectId())); assertTrue(client.getObjectDatabase().has(small.toObjectId()));
} }
abstract class TreeBuilder {
abstract void addElements(DirCacheBuilder dcBuilder) throws Exception;
RevTree build() throws Exception {
DirCache dc = DirCache.newInCore();
DirCacheBuilder dcBuilder = dc.builder();
addElements(dcBuilder);
dcBuilder.finish();
ObjectId id;
try (ObjectInserter ins =
remote.getRepository().newObjectInserter()) {
id = dc.writeTree(ins);
ins.flush();
}
return remote.getRevWalk().parseTree(id);
}
}
class DeepTreePreparator {
RevBlob blobLowDepth = remote.blob("lo");
RevBlob blobHighDepth = remote.blob("hi");
RevTree subtree = remote.tree(remote.file("1", blobHighDepth));
RevTree rootTree = (new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.add(remote.file("1", blobLowDepth));
dcBuilder.addTree(new byte[] {'2'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree);
}
}).build();
RevCommit commit = remote.commit(rootTree);
DeepTreePreparator() throws Exception {}
}
private void uploadV2WithTreeDepthFilter(
long depth, ObjectId... wants) throws Exception {
server.getConfig().setBoolean("uploadpack", null, "allowfilter", true);
List<String> input = new ArrayList();
input.add("command=fetch\n");
input.add(PacketLineIn.DELIM);
for (ObjectId want : wants) {
input.add("want " + want.getName() + "\n");
}
input.add("filter tree:" + depth + "\n");
input.add("done\n");
input.add(PacketLineIn.END);
ByteArrayInputStream recvStream =
uploadPackV2(RequestPolicy.ANY, /*refFilter=*/null,
/*hook=*/null, input.toArray(new String[0]));
PacketLineIn pckIn = new PacketLineIn(recvStream);
assertThat(pckIn.readString(), is("packfile"));
parsePack(recvStream);
}
@Test
public void testV2FetchFilterTreeDepth0() throws Exception {
DeepTreePreparator preparator = new DeepTreePreparator();
remote.update("master", preparator.commit);
uploadV2WithTreeDepthFilter(0, preparator.commit.toObjectId());
assertFalse(client.getObjectDatabase()
.has(preparator.rootTree.toObjectId()));
assertFalse(client.getObjectDatabase()
.has(preparator.subtree.toObjectId()));
assertFalse(client.getObjectDatabase()
.has(preparator.blobLowDepth.toObjectId()));
assertFalse(client.getObjectDatabase()
.has(preparator.blobHighDepth.toObjectId()));
}
@Test
public void testV2FetchFilterTreeDepth1_serverHasBitmap() throws Exception {
DeepTreePreparator preparator = new DeepTreePreparator();
remote.update("master", preparator.commit);
// The bitmap should be ignored since we need to track the depth while
// traversing the trees.
generateBitmaps(server);
uploadV2WithTreeDepthFilter(1, preparator.commit.toObjectId());
assertTrue(client.getObjectDatabase()
.has(preparator.rootTree.toObjectId()));
assertFalse(client.getObjectDatabase()
.has(preparator.subtree.toObjectId()));
assertFalse(client.getObjectDatabase()
.has(preparator.blobLowDepth.toObjectId()));
assertFalse(client.getObjectDatabase()
.has(preparator.blobHighDepth.toObjectId()));
}
@Test
public void testV2FetchFilterTreeDepth2() throws Exception {
DeepTreePreparator preparator = new DeepTreePreparator();
remote.update("master", preparator.commit);
uploadV2WithTreeDepthFilter(2, preparator.commit.toObjectId());
assertTrue(client.getObjectDatabase()
.has(preparator.rootTree.toObjectId()));
assertTrue(client.getObjectDatabase()
.has(preparator.subtree.toObjectId()));
assertTrue(client.getObjectDatabase()
.has(preparator.blobLowDepth.toObjectId()));
assertFalse(client.getObjectDatabase()
.has(preparator.blobHighDepth.toObjectId()));
}
/**
* Creates a commit with the following files:
* <pre>
* a/x/b/foo
* x/b/foo
* </pre>
* which has an identical tree in two locations: once at / and once at /a
*/
class RepeatedSubtreePreparator {
RevBlob foo = remote.blob("foo");
RevTree subtree3 = remote.tree(remote.file("foo", foo));
RevTree subtree2 = (new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'b'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree3);
}
}).build();
RevTree subtree1 = (new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'x'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree2);
}
}).build();
RevTree rootTree = (new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'a'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree1);
dcBuilder.addTree(new byte[] {'x'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree2);
}
}).build();
RevCommit commit = remote.commit(rootTree);
RepeatedSubtreePreparator() throws Exception {}
}
@Test
public void testV2FetchFilterTreeDepth_iterateOverTreeAtTwoLevels()
throws Exception {
// Test tree:<depth> where a tree is iterated to twice - once where a
// subentry is too deep to be included, and again where the blob inside
// it is shallow enough to be included.
RepeatedSubtreePreparator preparator = new RepeatedSubtreePreparator();
remote.update("master", preparator.commit);
uploadV2WithTreeDepthFilter(4, preparator.commit.toObjectId());
assertTrue(client.getObjectDatabase()
.has(preparator.foo.toObjectId()));
}
@Test
public void testWantFilteredObject() throws Exception {
RepeatedSubtreePreparator preparator = new RepeatedSubtreePreparator();
remote.update("master", preparator.commit);
// Specify wanted blob objects that are deep enough to be filtered. We
// should still upload them.
uploadV2WithTreeDepthFilter(
3,
preparator.commit.toObjectId(),
preparator.foo.toObjectId());
assertTrue(client.getObjectDatabase()
.has(preparator.foo.toObjectId()));
client = newRepo("client");
// Specify a wanted tree object that is deep enough to be filtered. We
// should still upload it.
uploadV2WithTreeDepthFilter(
2,
preparator.commit.toObjectId(),
preparator.subtree3.toObjectId());
assertTrue(client.getObjectDatabase()
.has(preparator.foo.toObjectId()));
assertTrue(client.getObjectDatabase()
.has(preparator.subtree3.toObjectId()));
}
@Test @Test
public void testV2FetchFilterWhenNotAllowed() throws Exception { public void testV2FetchFilterWhenNotAllowed() throws Exception {
RevCommit commit = remote.commit().message("0").create(); RevCommit commit = remote.commit().message("0").create();

54
org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java

@ -872,6 +872,21 @@ public class PackWriter implements AutoCloseable {
: new ObjectWalk(reader); : new ObjectWalk(reader);
} }
/**
* A visitation policy which causes objects to be visited repeatedly by
* making {@code shouldVisit} always return {@code true}.
*/
private static final ObjectWalk.VisitationPolicy ALWAYS_VISIT_POLICY =
new ObjectWalk.VisitationPolicy() {
@Override
public boolean shouldVisit(RevObject o) {
return true;
}
@Override
public void visited(RevObject o) {}
};
/** /**
* Prepare the list of objects to be written to the pack stream. * Prepare the list of objects to be written to the pack stream.
* <p> * <p>
@ -913,6 +928,9 @@ public class PackWriter implements AutoCloseable {
if (shallowPack && !(walk instanceof DepthWalk.ObjectWalk)) if (shallowPack && !(walk instanceof DepthWalk.ObjectWalk))
throw new IllegalArgumentException( throw new IllegalArgumentException(
JGitText.get().shallowPacksRequireDepthWalk); JGitText.get().shallowPacksRequireDepthWalk);
if (filterSpec.getTreeDepthLimit() >= 0) {
walk.setVisitationPolicy(ALWAYS_VISIT_POLICY);
}
findObjectsToPack(countingMonitor, walk, interestingObjects, findObjectsToPack(countingMonitor, walk, interestingObjects,
uninterestingObjects, noBitmaps); uninterestingObjects, noBitmaps);
} }
@ -1972,7 +1990,9 @@ public class PackWriter implements AutoCloseable {
byte[] pathBuf = walker.getPathBuffer(); byte[] pathBuf = walker.getPathBuffer();
int pathLen = walker.getPathLength(); int pathLen = walker.getPathLength();
bases.addBase(o.getType(), pathBuf, pathLen, pathHash); bases.addBase(o.getType(), pathBuf, pathLen, pathHash);
filterAndAddObject(o, o.getType(), pathHash, want); if (!depthSkip(o, walker)) {
filterAndAddObject(o, o.getType(), pathHash, want);
}
countingMonitor.update(1); countingMonitor.update(1);
} }
} else { } else {
@ -1982,7 +2002,10 @@ public class PackWriter implements AutoCloseable {
continue; continue;
if (exclude(o)) if (exclude(o))
continue; continue;
filterAndAddObject(o, o.getType(), walker.getPathHashCode(), want); if (!depthSkip(o, walker)) {
filterAndAddObject(o, o.getType(), walker.getPathHashCode(),
want);
}
countingMonitor.update(1); countingMonitor.update(1);
} }
} }
@ -2074,6 +2097,33 @@ public class PackWriter implements AutoCloseable {
objectsMap.add(otp); objectsMap.add(otp);
} }
/**
* Determines if the object should be omitted from the pack as a result of
* its depth (probably because of the tree:<depth> filter).
*
* @param obj
* the object to check whether it should be omitted.
* @param walker
* the walker being used for traveresal.
* @return whether the given object should be skipped.
*/
private boolean depthSkip(@NonNull RevObject obj, ObjectWalk walker) {
long treeDepth = walker.getTreeDepth();
// Check if this object needs to be rejected because it is a tree or
// blob that is too deep from the root tree.
// A blob is considered one level deeper than the tree that contains it.
if (obj.getType() == OBJ_BLOB) {
treeDepth++;
}
// TODO: Do not continue traversing the tree, since its children
// will also be too deep.
return filterSpec.getTreeDepthLimit() != -1 &&
treeDepth > filterSpec.getTreeDepthLimit();
}
// Adds the given object as an object to be packed, first performing // Adds the given object as an object to be packed, first performing
// filtering on blobs at or exceeding a given size. // filtering on blobs at or exceeding a given size.
private void filterAndAddObject(@NonNull AnyObjectId src, int type, private void filterAndAddObject(@NonNull AnyObjectId src, int type,

107
org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java

@ -43,6 +43,7 @@
package org.eclipse.jgit.revwalk; package org.eclipse.jgit.revwalk;
import static java.util.Objects.requireNonNull;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
import static org.eclipse.jgit.lib.Constants.OBJ_TREE; import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
@ -97,6 +98,55 @@ public class ObjectWalk extends RevWalk {
*/ */
private static final int IN_PENDING = RevWalk.REWRITE; private static final int IN_PENDING = RevWalk.REWRITE;
/**
* When walking over a tree and blob graph, objects are usually marked as
* seen as they are visited and this "seen" status is checked upon the next
* visit. If they are already "seen" then they are not processed (returned
* by {@link ObjectWalk#nextObject()}) again. However, this behavior can be
* overridden by supplying a different implementation of this class.
*
* @since 5.4
*/
public interface VisitationPolicy {
/**
* Whenever the rev or object walk reaches a Git object, if that object
* already exists as a RevObject, this method is called to determine if
* that object should be visited.
*
* @param o
* the object to check if it should be visited
* @return true if the object should be visited
*/
boolean shouldVisit(RevObject o);
/**
* Called when an object is visited.
*
* @param o
* the object that was visited
*/
void visited(RevObject o);
}
/**
* The default visitation policy: causes all objects to be visited exactly
* once.
*
* @since 5.4
*/
public static final VisitationPolicy SIMPLE_VISITATION_POLICY =
new VisitationPolicy() {
@Override
public boolean shouldVisit(RevObject o) {
return (o.flags & SEEN) == 0;
}
@Override
public void visited(RevObject o) {
o.flags |= SEEN;
}
};
private List<RevObject> rootObjects; private List<RevObject> rootObjects;
private BlockObjQueue pendingObjects; private BlockObjQueue pendingObjects;
@ -113,6 +163,8 @@ public class ObjectWalk extends RevWalk {
private boolean boundary; private boolean boundary;
private VisitationPolicy visitationPolicy = SIMPLE_VISITATION_POLICY;
/** /**
* Create a new revision and object walker for a given repository. * Create a new revision and object walker for a given repository.
* *
@ -299,6 +351,18 @@ public class ObjectWalk extends RevWalk {
objectFilter = newFilter != null ? newFilter : ObjectFilter.ALL; objectFilter = newFilter != null ? newFilter : ObjectFilter.ALL;
} }
/**
* Sets the visitation policy to use during this walk.
*
* @param policy
* the {@code VisitationPolicy} to use
* @since 5.4
*/
public void setVisitationPolicy(VisitationPolicy policy) {
assertNotStarted();
visitationPolicy = requireNonNull(policy);
}
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public RevCommit next() throws MissingObjectException, public RevCommit next() throws MissingObjectException,
@ -357,24 +421,23 @@ public class ObjectWalk extends RevWalk {
} }
RevObject obj = objects.get(idBuffer); RevObject obj = objects.get(idBuffer);
if (obj != null && (obj.flags & SEEN) != 0) if (obj != null && !visitationPolicy.shouldVisit(obj))
continue; continue;
int mode = parseMode(buf, startPtr, ptr, tv); int mode = parseMode(buf, startPtr, ptr, tv);
int flags;
switch (mode >>> TYPE_SHIFT) { switch (mode >>> TYPE_SHIFT) {
case TYPE_FILE: case TYPE_FILE:
case TYPE_SYMLINK: case TYPE_SYMLINK:
if (obj == null) { if (obj == null) {
obj = new RevBlob(idBuffer); obj = new RevBlob(idBuffer);
obj.flags = SEEN; visitationPolicy.visited(obj);
objects.add(obj); objects.add(obj);
return obj; return obj;
} }
if (!(obj instanceof RevBlob)) if (!(obj instanceof RevBlob))
throw new IncorrectObjectTypeException(obj, OBJ_BLOB); throw new IncorrectObjectTypeException(obj, OBJ_BLOB);
obj.flags = flags = obj.flags | SEEN; visitationPolicy.visited(obj);
if ((flags & UNINTERESTING) == 0) if ((obj.flags & UNINTERESTING) == 0)
return obj; return obj;
if (boundary) if (boundary)
return obj; return obj;
@ -383,14 +446,14 @@ public class ObjectWalk extends RevWalk {
case TYPE_TREE: case TYPE_TREE:
if (obj == null) { if (obj == null) {
obj = new RevTree(idBuffer); obj = new RevTree(idBuffer);
obj.flags = SEEN; visitationPolicy.visited(obj);
objects.add(obj); objects.add(obj);
return pushTree(obj); return pushTree(obj);
} }
if (!(obj instanceof RevTree)) if (!(obj instanceof RevTree))
throw new IncorrectObjectTypeException(obj, OBJ_TREE); throw new IncorrectObjectTypeException(obj, OBJ_TREE);
obj.flags = flags = obj.flags | SEEN; visitationPolicy.visited(obj);
if ((flags & UNINTERESTING) == 0) if ((obj.flags & UNINTERESTING) == 0)
return pushTree(obj); return pushTree(obj);
if (boundary) if (boundary)
return pushTree(obj); return pushTree(obj);
@ -419,12 +482,11 @@ public class ObjectWalk extends RevWalk {
if (o == null) { if (o == null) {
return null; return null;
} }
int flags = o.flags; if (!visitationPolicy.shouldVisit(o)) {
if ((flags & SEEN) != 0)
continue; continue;
flags |= SEEN; }
o.flags = flags; visitationPolicy.visited(o);
if ((flags & UNINTERESTING) == 0 | boundary) { if ((o.flags & UNINTERESTING) == 0 | boundary) {
if (o instanceof RevTree) { if (o instanceof RevTree) {
// The previous while loop should have exhausted the stack // The previous while loop should have exhausted the stack
// of trees. // of trees.
@ -575,6 +637,17 @@ public class ObjectWalk extends RevWalk {
return RawParseUtils.decode(pathBuf, 0, pathLen); return RawParseUtils.decode(pathBuf, 0, pathLen);
} }
/**
* @return the current traversal depth from the root tree object
* @since 5.4
*/
public int getTreeDepth() {
if (currVisit == null) {
return 0;
}
return currVisit.depth;
}
/** /**
* Get the current object's path hash code. * Get the current object's path hash code.
* <p> * <p>
@ -778,6 +851,11 @@ public class ObjectWalk extends RevWalk {
tv.buf = reader.open(obj, OBJ_TREE).getCachedBytes(); tv.buf = reader.open(obj, OBJ_TREE).getCachedBytes();
tv.parent = currVisit; tv.parent = currVisit;
currVisit = tv; currVisit = tv;
if (tv.parent == null) {
tv.depth = 1;
} else {
tv.depth = tv.parent.depth + 1;
}
return obj; return obj;
} }
@ -809,5 +887,8 @@ public class ObjectWalk extends RevWalk {
/** Number of bytes in the path leading up to this tree. */ /** Number of bytes in the path leading up to this tree. */
int pathLen; int pathLen;
/** Number of levels deep from the root tree. 0 for root tree. */
int depth;
} }
} }

80
org.eclipse.jgit/src/org/eclipse/jgit/transport/FilterSpec.java

@ -59,13 +59,22 @@ public final class FilterSpec {
private final long blobLimit; private final long blobLimit;
private FilterSpec(long blobLimit) { private final long treeDepthLimit;
private FilterSpec(long blobLimit, long treeDepthLimit) {
this.blobLimit = blobLimit; this.blobLimit = blobLimit;
this.treeDepthLimit = treeDepthLimit;
} }
/** /**
* Process the content of "filter" line from the protocol. It has a shape * Process the content of "filter" line from the protocol. It has a shape
* like "blob:none" or "blob:limit=N", with limit a positive number. * like:
*
* <ul>
* <li>"blob:none"
* <li>"blob:limit=N", with N &gt;= 0
* <li>"tree:DEPTH", with DEPTH &gt;= 0
* </ul>
* *
* @param filterLine * @param filterLine
* the content of the "filter" line in the protocol * the content of the "filter" line in the protocol
@ -76,31 +85,37 @@ public final class FilterSpec {
*/ */
public static FilterSpec fromFilterLine(String filterLine) public static FilterSpec fromFilterLine(String filterLine)
throws PackProtocolException { throws PackProtocolException {
long blobLimit = -1;
if (filterLine.equals("blob:none")) { //$NON-NLS-1$ if (filterLine.equals("blob:none")) { //$NON-NLS-1$
blobLimit = 0; return FilterSpec.withBlobLimit(0);
} else if (filterLine.startsWith("blob:limit=")) { //$NON-NLS-1$ } else if (filterLine.startsWith("blob:limit=")) { //$NON-NLS-1$
long blobLimit = -1;
try { try {
blobLimit = Long blobLimit = Long
.parseLong(filterLine.substring("blob:limit=".length())); //$NON-NLS-1$ .parseLong(filterLine.substring("blob:limit=".length())); //$NON-NLS-1$
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new PackProtocolException(MessageFormat // Do not change blobLimit so that we throw a
.format(JGitText.get().invalidFilter, filterLine)); // PackProtocolException later.
}
if (blobLimit >= 0) {
return FilterSpec.withBlobLimit(blobLimit);
}
} else if (filterLine.startsWith("tree:")) { //$NON-NLS-1$
long treeDepthLimit = -1;
try {
treeDepthLimit = Long
.parseLong(filterLine.substring("tree:".length())); //$NON-NLS-1$
} catch (NumberFormatException e) {
// Do not change blobLimit so that we throw a
// PackProtocolException later.
}
if (treeDepthLimit >= 0) {
return FilterSpec.withTreeDepthLimit(treeDepthLimit);
} }
}
/*
* We must have (1) either "blob:none" or "blob:limit=" set (because we
* only support blob size limits for now), and (2) if the latter, then
* it must be nonnegative. Throw if (1) or (2) is not met.
*/
if (blobLimit < 0) {
throw new PackProtocolException(
MessageFormat.format(
JGitText.get().invalidFilter, filterLine));
} }
return new FilterSpec(blobLimit); // Did not match any known filter format.
throw new PackProtocolException(
MessageFormat.format(JGitText.get().invalidFilter, filterLine));
} }
/** /**
@ -113,13 +128,27 @@ public final class FilterSpec {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"blobLimit cannot be negative: " + blobLimit); //$NON-NLS-1$ "blobLimit cannot be negative: " + blobLimit); //$NON-NLS-1$
} }
return new FilterSpec(blobLimit); return new FilterSpec(blobLimit, -1);
}
/**
* @param treeDepthLimit
* the tree depth limit in a "tree:[depth]" filter line
* @return a filter spec which filters blobs and trees beyond a certain tree
* depth
*/
static FilterSpec withTreeDepthLimit(long treeDepthLimit) {
if (treeDepthLimit < 0) {
throw new IllegalArgumentException(
"treeDepthLimit cannot be negative: " + treeDepthLimit); //$NON-NLS-1$
}
return new FilterSpec(-1, treeDepthLimit);
} }
/** /**
* A placeholder that indicates no filtering. * A placeholder that indicates no filtering.
*/ */
public static final FilterSpec NO_FILTER = new FilterSpec(-1); public static final FilterSpec NO_FILTER = new FilterSpec(-1, -1);
/** /**
* @return -1 if this filter does not filter blobs based on size, or a * @return -1 if this filter does not filter blobs based on size, or a
@ -129,11 +158,20 @@ public final class FilterSpec {
return blobLimit; return blobLimit;
} }
/**
* @return -1 if this filter does not filter blobs and trees based on depth,
* or a non-negative integer representing the max tree depth of
* blobs and trees to fetch
*/
public long getTreeDepthLimit() {
return treeDepthLimit;
}
/** /**
* @return true if this filter doesn't filter out anything * @return true if this filter doesn't filter out anything
*/ */
public boolean isNoOp() { public boolean isNoOp() {
return blobLimit == -1; return blobLimit == -1 && treeDepthLimit == -1;
} }
/** /**

3
org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java

@ -2110,7 +2110,8 @@ public class UploadPack {
} }
pw.setUseBitmaps( pw.setUseBitmaps(
req.getDepth() == 0 req.getDepth() == 0
&& req.getClientShallowCommits().isEmpty()); && req.getClientShallowCommits().isEmpty()
&& req.getFilterSpec().getTreeDepthLimit() == -1);
pw.setClientShallowCommits(req.getClientShallowCommits()); pw.setClientShallowCommits(req.getClientShallowCommits());
pw.setReuseDeltaCommits(true); pw.setReuseDeltaCommits(true);
pw.setDeltaBaseAsOffset( pw.setDeltaBaseAsOffset(

Loading…
Cancel
Save