Browse Source

Add seekPastPrefix method to RefCursor

This method will be used by the follow-up change. This useful if we want
to go over all the changes after a specific ref.

For example, the new method allows us to create a follow-up that would
go over all the refs until we reach a specific ref (e.g refs/changes/),
and then we use seekPastPrefix(refs/changes/) to read the rest of the refs,
thus basically we return all refs except a specific prefix.

When seeking past a prefix, the previous condition that created the
RefCursor still applies. E.g, if the cursor was created by
seekRefsWithPrefix, we can skip some refs but we will not return refs
that are not starting with this prefix.

Signed-off-by: Gal Paikin <paiking@google.com>
Change-Id: I2c02e89c877fe90da8619cb8a4a9a0c865f238ef
tags/v5.11.0.202102031030-m2
Gal Paikin 3 years ago
parent
commit
68b95afc70

+ 112
- 0
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/MergedReftableTest.java View File

@@ -137,6 +137,118 @@ public class MergedReftableTest {
}
}

@Test
public void twoTableSeekPastWithRefCursor() throws IOException {
List<Ref> delta1 = Arrays.asList(
ref("refs/heads/apple", 1),
ref("refs/heads/master", 2));
List<Ref> delta2 = Arrays.asList(
ref("refs/heads/banana", 3),
ref("refs/heads/zzlast", 4));

MergedReftable mr = merge(write(delta1), write(delta2));
try (RefCursor rc = mr.seekRefsWithPrefix("")) {
assertTrue(rc.next());
assertEquals("refs/heads/apple", rc.getRef().getName());
assertEquals(id(1), rc.getRef().getObjectId());

rc.seekPastPrefix("refs/heads/banana/");

assertTrue(rc.next());
assertEquals("refs/heads/master", rc.getRef().getName());
assertEquals(id(2), rc.getRef().getObjectId());

assertTrue(rc.next());
assertEquals("refs/heads/zzlast", rc.getRef().getName());
assertEquals(id(4), rc.getRef().getObjectId());

assertEquals(1, rc.getRef().getUpdateIndex());
}
}

@Test
public void oneTableSeekPastWithRefCursor() throws IOException {
List<Ref> delta1 = Arrays.asList(
ref("refs/heads/apple", 1),
ref("refs/heads/master", 2));

MergedReftable mr = merge(write(delta1));
try (RefCursor rc = mr.seekRefsWithPrefix("")) {
rc.seekPastPrefix("refs/heads/apple");

assertTrue(rc.next());
assertEquals("refs/heads/master", rc.getRef().getName());
assertEquals(id(2), rc.getRef().getObjectId());

assertEquals(1, rc.getRef().getUpdateIndex());
}
}

@Test
public void seekPastToNonExistentPrefixToTheMiddle() throws IOException {
List<Ref> delta1 = Arrays.asList(
ref("refs/heads/apple", 1),
ref("refs/heads/master", 2));
List<Ref> delta2 = Arrays.asList(
ref("refs/heads/banana", 3),
ref("refs/heads/zzlast", 4));

MergedReftable mr = merge(write(delta1), write(delta2));
try (RefCursor rc = mr.seekRefsWithPrefix("")) {
rc.seekPastPrefix("refs/heads/x");

assertTrue(rc.next());
assertEquals("refs/heads/zzlast", rc.getRef().getName());
assertEquals(id(4), rc.getRef().getObjectId());

assertEquals(1, rc.getRef().getUpdateIndex());
}
}

@Test
public void seekPastToNonExistentPrefixToTheEnd() throws IOException {
List<Ref> delta1 = Arrays.asList(
ref("refs/heads/apple", 1),
ref("refs/heads/master", 2));
List<Ref> delta2 = Arrays.asList(
ref("refs/heads/banana", 3),
ref("refs/heads/zzlast", 4));

MergedReftable mr = merge(write(delta1), write(delta2));
try (RefCursor rc = mr.seekRefsWithPrefix("")) {
rc.seekPastPrefix("refs/heads/zzz");
assertFalse(rc.next());
}
}

@Test
public void seekPastManyTimes() throws IOException {
List<Ref> delta1 = Arrays.asList(
ref("refs/heads/apple", 1),
ref("refs/heads/master", 2));
List<Ref> delta2 = Arrays.asList(
ref("refs/heads/banana", 3),
ref("refs/heads/zzlast", 4));

MergedReftable mr = merge(write(delta1), write(delta2));
try (RefCursor rc = mr.seekRefsWithPrefix("")) {
rc.seekPastPrefix("refs/heads/apple");
rc.seekPastPrefix("refs/heads/banana");
rc.seekPastPrefix("refs/heads/master");
rc.seekPastPrefix("refs/heads/zzlast");
assertFalse(rc.next());
}
}

@Test
public void seekPastOnEmptyTable() throws IOException {
MergedReftable mr = merge(write(), write());
try (RefCursor rc = mr.seekRefsWithPrefix("")) {
rc.seekPastPrefix("refs/");
assertFalse(rc.next());
}
}

@Test
public void twoTableById() throws IOException {
List<Ref> delta1 = Arrays.asList(

+ 158
- 0
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java View File

@@ -10,6 +10,7 @@

package org.eclipse.jgit.internal.storage.reftable;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.HEAD;
import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
import static org.eclipse.jgit.lib.Constants.R_HEADS;
@@ -49,8 +50,16 @@ import org.hamcrest.Matchers;
import org.junit.Test;

public class ReftableTest {
private static final byte[] LAST_UTF8_CHAR = new byte[] {
(byte)0x10,
(byte)0xFF,
(byte)0xFF};

private static final String MASTER = "refs/heads/master";
private static final String NEXT = "refs/heads/next";
private static final String AFTER_NEXT = "refs/heads/nextnext";
private static final String LAST = "refs/heads/nextnextnext";
private static final String NOT_REF_HEADS = "refs/zzz/zzz";
private static final String V1_0 = "refs/tags/v1.0";

private Stats stats;
@@ -395,6 +404,135 @@ public class ReftableTest {
}
}

@Test
public void seekPastRefWithRefCursor() throws IOException {
Ref exp = ref(MASTER, 1);
Ref next = ref(NEXT, 2);
Ref afterNext = ref(AFTER_NEXT, 3);
Ref afterNextNext = ref(LAST, 4);
ReftableReader t = read(write(exp, next, afterNext, afterNextNext));
try (RefCursor rc = t.seekRefsWithPrefix("")) {
assertTrue(rc.next());
assertEquals(MASTER, rc.getRef().getName());

rc.seekPastPrefix("refs/heads/next/");

assertTrue(rc.next());
assertEquals(AFTER_NEXT, rc.getRef().getName());
assertTrue(rc.next());
assertEquals(LAST, rc.getRef().getName());

assertFalse(rc.next());
}
}

@Test
public void seekPastToNonExistentPrefixToTheMiddle() throws IOException {
Ref exp = ref(MASTER, 1);
Ref next = ref(NEXT, 2);
Ref afterNext = ref(AFTER_NEXT, 3);
Ref afterNextNext = ref(LAST, 4);
ReftableReader t = read(write(exp, next, afterNext, afterNextNext));
try (RefCursor rc = t.seekRefsWithPrefix("")) {
rc.seekPastPrefix("refs/heads/master_non_existent");

assertTrue(rc.next());
assertEquals(NEXT, rc.getRef().getName());

assertTrue(rc.next());
assertEquals(AFTER_NEXT, rc.getRef().getName());

assertTrue(rc.next());
assertEquals(LAST, rc.getRef().getName());

assertFalse(rc.next());
}
}

@Test
public void seekPastToNonExistentPrefixToTheEnd() throws IOException {
Ref exp = ref(MASTER, 1);
Ref next = ref(NEXT, 2);
Ref afterNext = ref(AFTER_NEXT, 3);
Ref afterNextNext = ref(LAST, 4);
ReftableReader t = read(write(exp, next, afterNext, afterNextNext));
try (RefCursor rc = t.seekRefsWithPrefix("")) {
rc.seekPastPrefix("refs/heads/nextnon_existent_end");
assertFalse(rc.next());
}
}

@Test
public void seekPastWithSeekRefsWithPrefix() throws IOException {
Ref exp = ref(MASTER, 1);
Ref next = ref(NEXT, 2);
Ref afterNext = ref(AFTER_NEXT, 3);
Ref afterNextNext = ref(LAST, 4);
Ref notRefsHeads = ref(NOT_REF_HEADS, 5);
ReftableReader t = read(write(exp, next, afterNext, afterNextNext, notRefsHeads));
try (RefCursor rc = t.seekRefsWithPrefix("refs/heads/")) {
rc.seekPastPrefix("refs/heads/next/");
assertTrue(rc.next());
assertEquals(AFTER_NEXT, rc.getRef().getName());
assertTrue(rc.next());
assertEquals(LAST, rc.getRef().getName());

// NOT_REF_HEADS is next, but it's omitted because of
// seekRefsWithPrefix("refs/heads/").
assertFalse(rc.next());
}
}

@Test
public void seekPastWithLotsOfRefs() throws IOException {
Ref[] refs = new Ref[500];
for (int i = 1; i <= 500; i++) {
refs[i - 1] = ref(String.format("refs/%d", i), i);
}
ReftableReader t = read(write(refs));
try (RefCursor rc = t.allRefs()) {
rc.seekPastPrefix("refs/3");
assertTrue(rc.next());
assertEquals("refs/4", rc.getRef().getName());
assertTrue(rc.next());
assertEquals("refs/40", rc.getRef().getName());

rc.seekPastPrefix("refs/8");
assertTrue(rc.next());
assertEquals("refs/9", rc.getRef().getName());
assertTrue(rc.next());
assertEquals("refs/90", rc.getRef().getName());
assertTrue(rc.next());
assertEquals("refs/91", rc.getRef().getName());
}
}

@Test
public void seekPastManyTimes() throws IOException {
Ref exp = ref(MASTER, 1);
Ref next = ref(NEXT, 2);
Ref afterNext = ref(AFTER_NEXT, 3);
Ref afterNextNext = ref(LAST, 4);
ReftableReader t = read(write(exp, next, afterNext, afterNextNext));

try (RefCursor rc = t.seekRefsWithPrefix("")) {
rc.seekPastPrefix("refs/heads/master");
rc.seekPastPrefix("refs/heads/next");
rc.seekPastPrefix("refs/heads/nextnext");
rc.seekPastPrefix("refs/heads/nextnextnext");
assertFalse(rc.next());
}
}

@Test
public void seekPastOnEmptyTable() throws IOException {
ReftableReader t = read(write());
try (RefCursor rc = t.seekRefsWithPrefix("")) {
rc.seekPastPrefix("refs/");
assertFalse(rc.next());
}
}

@Test
public void indexScan() throws IOException {
List<Ref> refs = new ArrayList<>();
@@ -873,6 +1011,14 @@ public class ReftableTest {
}
}

@Test
public void byObjectIdSkipPastPrefix() throws IOException {
ReftableReader t = read(write());
try (RefCursor rc = t.byObjectId(id(2))) {
assertThrows(UnsupportedOperationException.class, () -> rc.seekPastPrefix("refs/heads/"));
}
}

@Test
public void unpeeledDoesNotWrite() {
try {
@@ -883,6 +1029,18 @@ public class ReftableTest {
}
}

@Test
public void skipPastRefWithLastUTF8() throws IOException {
ReftableReader t = read(write(ref(String.format("refs/heads/%sbla", new String(LAST_UTF8_CHAR
, UTF_8)), 1)));

try (RefCursor rc = t.allRefs()) {
rc.seekPastPrefix("refs/heads/");
assertFalse(rc.next());
}
}


@Test
public void nameTooLongDoesNotWrite() throws IOException {
try {

+ 18
- 0
org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java View File

@@ -11,6 +11,7 @@
package org.eclipse.jgit.internal.storage.reftable;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;

@@ -215,6 +216,23 @@ public class MergedReftable extends Reftable {
}
}

@Override
public void seekPastPrefix(String prefixName) throws IOException {
List<RefQueueEntry> entriesToAdd = new ArrayList<>();
entriesToAdd.addAll(queue);
if (head != null) {
entriesToAdd.add(head);
}

head = null;
queue.clear();

for(RefQueueEntry entry : entriesToAdd){
entry.rc.seekPastPrefix(prefixName);
add(entry);
}
}

private RefQueueEntry poll() {
RefQueueEntry e = head;
if (e != null) {

+ 13
- 0
org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/RefCursor.java View File

@@ -28,6 +28,19 @@ public abstract class RefCursor implements AutoCloseable {
*/
public abstract boolean next() throws IOException;

/**
* Seeks forward to the first ref record lexicographically beyond
* {@code prefixName} that doesn't start with {@code prefixName}. If there are
* no more results, skipping some refs won't add new results. E.g if we create a
* RefCursor that returns only results with a specific prefix, skipping that
* prefix won't give results that are not part of the original prefix.
*
* @param prefixName prefix that should be skipped. All previous refs before it
* will be skipped.
* @throws java.io.IOException references cannot be read.
*/
public abstract void seekPastPrefix(String prefixName) throws IOException;

/**
* Get reference at the current position.
*

+ 26
- 0
org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java View File

@@ -508,6 +508,21 @@ public class ReftableReader extends Reftable implements AutoCloseable {
}
}

@Override
public void seekPastPrefix(String prefixName) throws IOException {
initRefIndex();
byte[] key = prefixName.getBytes(UTF_8);
ByteBuffer byteBuffer = ByteBuffer.allocate(key.length + 1);
byteBuffer.put(key);
// Add the representation of the last byte lexicographically. Based on how UTF_8
// representation works, this byte will be bigger lexicographically than any
// UTF_8 character when translated into bytes, since 0xFF can never be a part of
// a UTF_8 string.
byteBuffer.put((byte) 0xFF);

block = seek(REF_BLOCK_TYPE, byteBuffer.array(), refIndex, 0, refEnd);
}

@Override
public Ref getRef() {
return ref;
@@ -681,6 +696,17 @@ public class ReftableReader extends Reftable implements AutoCloseable {
}
}

@Override
/**
* The implementation here would not be efficient complexity-wise since it
* expected that there are a small number of refs that match the same object id.
* In such case it's better to not even use this method (as the caller might
* expect it to be efficient).
*/
public void seekPastPrefix(String prefixName) throws IOException {
throw new UnsupportedOperationException();
}

@Override
public Ref getRef() {
return ref;

Loading…
Cancel
Save