diff --git a/java/com/google/gerrit/server/cache/StringSerializer.java b/java/com/google/gerrit/server/cache/StringSerializer.java new file mode 100644 index 0000000000..1e456c7db9 --- /dev/null +++ b/java/com/google/gerrit/server/cache/StringSerializer.java @@ -0,0 +1,70 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.cache; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.annotations.VisibleForTesting; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; + +public enum StringSerializer implements CacheSerializer { + INSTANCE; + + @Override + public byte[] serialize(String object) { + return serialize(UTF_8, object); + } + + @VisibleForTesting + static byte[] serialize(Charset charset, String s) { + if (s.isEmpty()) { + return new byte[0]; + } + try { + ByteBuffer buf = + charset + .newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .encode(CharBuffer.wrap(s)); + byte[] result = new byte[buf.remaining()]; + buf.get(result); + return result; + } catch (CharacterCodingException e) { + throw new IllegalStateException("Failed to serialize string", e); + } + } + + @Override + public String deserialize(byte[] in) { + if (in.length == 0) { + return ""; + } + try { + return UTF_8 + .newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .decode(ByteBuffer.wrap(in)) + .toString(); + } catch (CharacterCodingException e) { + throw new IllegalStateException("Failed to deserialize string", e); + } + } +} diff --git a/java/com/google/gerrit/server/git/TagCache.java b/java/com/google/gerrit/server/git/TagCache.java index b8acd0a994..4d0e056244 100644 --- a/java/com/google/gerrit/server/git/TagCache.java +++ b/java/com/google/gerrit/server/git/TagCache.java @@ -17,14 +17,12 @@ package com.google.gerrit.server.git; import com.google.common.cache.Cache; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.cache.CacheModule; +import com.google.gerrit.server.cache.StringSerializer; import com.google.inject.Inject; import com.google.inject.Module; import com.google.inject.Singleton; import com.google.inject.name.Named; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; +import java.util.concurrent.ExecutionException; import org.eclipse.jgit.lib.ObjectId; @Singleton @@ -35,17 +33,19 @@ public class TagCache { return new CacheModule() { @Override protected void configure() { - persist(CACHE_NAME, String.class, EntryVal.class); + persist(CACHE_NAME, String.class, TagSetHolder.class) + .version(1) + .keySerializer(StringSerializer.INSTANCE) + .valueSerializer(TagSetHolder.Serializer.INSTANCE); bind(TagCache.class); } }; } - private final Cache cache; - private final Object createLock = new Object(); + private final Cache cache; @Inject - TagCache(@Named(CACHE_NAME) Cache cache) { + TagCache(@Named(CACHE_NAME) Cache cache) { this.cache = cache; } @@ -68,62 +68,26 @@ public class TagCache { // never fail with an exception. Some of these references can be null // (e.g. not all projects are cached, or the cache is not current). // - EntryVal val = cache.getIfPresent(name.get()); - if (val != null) { - TagSetHolder holder = val.holder; - if (holder != null) { - TagSet tags = holder.getTagSet(); - if (tags != null) { - if (tags.updateFastForward(refName, oldValue, newValue)) { - cache.put(name.get(), val); - } + TagSetHolder holder = cache.getIfPresent(name.get()); + if (holder != null) { + TagSet tags = holder.getTagSet(); + if (tags != null) { + if (tags.updateFastForward(refName, oldValue, newValue)) { + cache.put(name.get(), holder); } } } } public TagSetHolder get(Project.NameKey name) { - EntryVal val = cache.getIfPresent(name.get()); - if (val == null) { - synchronized (createLock) { - val = cache.getIfPresent(name.get()); - if (val == null) { - val = new EntryVal(); - val.holder = new TagSetHolder(name); - cache.put(name.get(), val); - } - } + try { + return cache.get(name.get(), () -> new TagSetHolder(name)); + } catch (ExecutionException e) { + throw new IllegalStateException(e); } - return val.holder; } void put(Project.NameKey name, TagSetHolder tags) { - EntryVal val = new EntryVal(); - val.holder = tags; - cache.put(name.get(), val); - } - - public static class EntryVal implements Serializable { - static final long serialVersionUID = 1L; - - transient TagSetHolder holder; - - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - holder = new TagSetHolder(new Project.NameKey(in.readUTF())); - if (in.readBoolean()) { - TagSet tags = new TagSet(holder.getProjectName()); - tags.readObject(in); - holder.setTagSet(tags); - } - } - - private void writeObject(ObjectOutputStream out) throws IOException { - TagSet tags = holder.getTagSet(); - out.writeUTF(holder.getProjectName().get()); - out.writeBoolean(tags != null); - if (tags != null) { - tags.writeObject(out); - } - } + cache.put(name.get(), tags); } } diff --git a/java/com/google/gerrit/server/git/TagSet.java b/java/com/google/gerrit/server/git/TagSet.java index 814da1b17b..436099852d 100644 --- a/java/com/google/gerrit/server/git/TagSet.java +++ b/java/com/google/gerrit/server/git/TagSet.java @@ -14,16 +14,19 @@ package com.google.gerrit.server.git; -import static org.eclipse.jgit.lib.ObjectIdSerializer.readWithoutMarker; -import static org.eclipse.jgit.lib.ObjectIdSerializer.writeWithoutMarker; - +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.collect.Maps; import com.google.common.flogger.FluentLogger; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter; +import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto; +import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.CachedRefProto; +import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.TagProto; +import com.google.protobuf.ByteString; import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.util.BitSet; import java.util.HashMap; import java.util.Map; @@ -47,15 +50,35 @@ class TagSet { private final ObjectIdOwnerMap tags; TagSet(Project.NameKey projectName) { + this(projectName, new HashMap<>(), new ObjectIdOwnerMap<>()); + } + + TagSet(Project.NameKey projectName, HashMap refs, ObjectIdOwnerMap tags) { this.projectName = projectName; - this.refs = new HashMap<>(); - this.tags = new ObjectIdOwnerMap<>(); + this.refs = refs; + this.tags = tags; + } + + Project.NameKey getProjectName() { + return projectName; } Tag lookupTag(AnyObjectId id) { return tags.get(id); } + // Test methods have obtuse names in addition to annotations, since they expose mutable state + // which would be easy to corrupt. + @VisibleForTesting + Map getRefsForTesting() { + return refs; + } + + @VisibleForTesting + ObjectIdOwnerMap getTagsForTesting() { + return tags; + } + boolean updateFastForward(String refName, ObjectId oldValue, ObjectId newValue) { CachedRef ref = refs.get(refName); if (ref != null) { @@ -191,36 +214,46 @@ class TagSet { } } - void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - int refCnt = in.readInt(); - for (int i = 0; i < refCnt; i++) { - String name = in.readUTF(); - int flag = in.readInt(); - ObjectId id = readWithoutMarker(in); - refs.put(name, new CachedRef(flag, id)); - } + static TagSet fromProto(TagSetProto proto) { + ObjectIdConverter idConverter = ObjectIdConverter.create(); - int tagCnt = in.readInt(); - for (int i = 0; i < tagCnt; i++) { - ObjectId id = readWithoutMarker(in); - BitSet flags = (BitSet) in.readObject(); - tags.add(new Tag(id, flags)); - } + HashMap refs = Maps.newHashMapWithExpectedSize(proto.getRefCount()); + proto + .getRefMap() + .forEach( + (n, cr) -> + refs.put(n, new CachedRef(cr.getFlag(), idConverter.fromByteString(cr.getId())))); + ObjectIdOwnerMap tags = new ObjectIdOwnerMap<>(); + proto + .getTagList() + .forEach( + t -> + tags.add( + new Tag( + idConverter.fromByteString(t.getId()), + BitSet.valueOf(t.getFlags().asReadOnlyByteBuffer())))); + return new TagSet(new Project.NameKey(proto.getProjectName()), refs, tags); } - void writeObject(ObjectOutputStream out) throws IOException { - out.writeInt(refs.size()); - for (Map.Entry e : refs.entrySet()) { - out.writeUTF(e.getKey()); - out.writeInt(e.getValue().flag); - writeWithoutMarker(out, e.getValue().get()); - } - - out.writeInt(tags.size()); - for (Tag tag : tags) { - writeWithoutMarker(out, tag); - out.writeObject(tag.refFlags); - } + TagSetProto toProto() { + ObjectIdConverter idConverter = ObjectIdConverter.create(); + TagSetProto.Builder b = TagSetProto.newBuilder().setProjectName(projectName.get()); + refs.forEach( + (n, cr) -> + b.putRef( + n, + CachedRefProto.newBuilder() + .setId(idConverter.toByteString(cr.get())) + .setFlag(cr.flag) + .build())); + tags.forEach( + t -> + b.addTag( + TagProto.newBuilder() + .setId(idConverter.toByteString(t)) + .setFlags(ByteString.copyFrom(t.refFlags.toByteArray())) + .build())); + return b.build(); } private boolean refresh(TagSet old, TagMatcher m) { @@ -341,8 +374,17 @@ class TagSet { return ref.getName().startsWith(Constants.R_TAGS); } + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("projectName", projectName) + .add("refs", refs) + .add("tags", tags) + .toString(); + } + static final class Tag extends ObjectIdOwnerMap.Entry { - private final BitSet refFlags; + @VisibleForTesting final BitSet refFlags; Tag(AnyObjectId id, BitSet flags) { super(id); @@ -352,11 +394,15 @@ class TagSet { boolean has(BitSet mask) { return refFlags.intersects(mask); } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).addValue(name()).add("refFlags", refFlags).toString(); + } } - private static final class CachedRef extends AtomicReference { - private static final long serialVersionUID = 1L; - + @VisibleForTesting + static final class CachedRef extends AtomicReference { final int flag; CachedRef(Ref ref, int flag) { @@ -367,6 +413,15 @@ class TagSet { this.flag = flag; set(id); } + + @Override + public String toString() { + ObjectId id = get(); + return MoreObjects.toStringHelper(this) + .addValue(id != null ? id.name() : "null") + .add("flag", flag) + .toString(); + } } private static final class TagWalk extends RevWalk { diff --git a/java/com/google/gerrit/server/git/TagSetHolder.java b/java/com/google/gerrit/server/git/TagSetHolder.java index 3f08d104c7..0790a36d6b 100644 --- a/java/com/google/gerrit/server/git/TagSetHolder.java +++ b/java/com/google/gerrit/server/git/TagSetHolder.java @@ -16,7 +16,11 @@ package com.google.gerrit.server.git; import static java.util.stream.Collectors.toList; +import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.cache.CacheSerializer; +import com.google.gerrit.server.cache.ProtoCacheSerializers; +import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto; import java.util.Collection; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; @@ -24,7 +28,8 @@ import org.eclipse.jgit.lib.Repository; public class TagSetHolder { private final Object buildLock = new Object(); private final Project.NameKey projectName; - private volatile TagSet tags; + + @Nullable private volatile TagSet tags; TagSetHolder(Project.NameKey projectName) { this.projectName = projectName; @@ -94,4 +99,30 @@ public class TagSetHolder { return cur; } } + + enum Serializer implements CacheSerializer { + INSTANCE; + + @Override + public byte[] serialize(TagSetHolder object) { + TagSetHolderProto.Builder b = + TagSetHolderProto.newBuilder().setProjectName(object.projectName.get()); + TagSet tags = object.tags; + if (tags != null) { + b.setTags(tags.toProto()); + } + return ProtoCacheSerializers.toByteArray(b.build()); + } + + @Override + public TagSetHolder deserialize(byte[] in) { + TagSetHolderProto proto = + ProtoCacheSerializers.parseUnchecked(TagSetHolderProto.parser(), in); + TagSetHolder holder = new TagSetHolder(new Project.NameKey(proto.getProjectName())); + if (proto.hasTags()) { + holder.tags = TagSet.fromProto(proto.getTags()); + } + return holder; + } + } } diff --git a/javatests/com/google/gerrit/server/cache/StringSerializerTest.java b/javatests/com/google/gerrit/server/cache/StringSerializerTest.java new file mode 100644 index 0000000000..303533867e --- /dev/null +++ b/javatests/com/google/gerrit/server/cache/StringSerializerTest.java @@ -0,0 +1,63 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.cache; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assert_; + +import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; +import org.junit.Test; + +public class StringSerializerTest { + @Test + public void serialize() { + assertThat(StringSerializer.INSTANCE.serialize("")).isEmpty(); + assertThat(StringSerializer.INSTANCE.serialize("abc")).isEqualTo(new byte[] {'a', 'b', 'c'}); + assertThat(StringSerializer.INSTANCE.serialize("a\u1234c")) + .isEqualTo(new byte[] {'a', (byte) 0xe1, (byte) 0x88, (byte) 0xb4, 'c'}); + } + + @Test + public void serializeInvalidChar() { + // Can't use UTF-8 for the test, since it can encode all Unicode code points. + try { + StringSerializer.serialize(StandardCharsets.US_ASCII, "\u1234"); + assert_().fail("expected IllegalStateException"); + } catch (IllegalStateException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CharacterCodingException.class); + } + } + + @Test + public void deserialize() { + assertThat(StringSerializer.INSTANCE.deserialize(new byte[0])).isEmpty(); + assertThat(StringSerializer.INSTANCE.deserialize(new byte[] {'a', 'b', 'c'})).isEqualTo("abc"); + assertThat( + StringSerializer.INSTANCE.deserialize( + new byte[] {'a', (byte) 0xe1, (byte) 0x88, (byte) 0xb4, 'c'})) + .isEqualTo("a\u1234c"); + } + + @Test + public void deserializeInvalidChar() { + try { + StringSerializer.INSTANCE.deserialize(new byte[] {(byte) 0xff}); + assert_().fail("expected IllegalStateException"); + } catch (IllegalStateException expected) { + assertThat(expected).hasCauseThat().isInstanceOf(CharacterCodingException.class); + } + } +} diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java index 4180192e6e..9bba9962cc 100644 --- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java +++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java @@ -15,12 +15,11 @@ package com.google.gerrit.server.cache.h2; import static com.google.common.truth.Truth.assertThat; -import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.util.concurrent.MoreExecutors; -import com.google.gerrit.server.cache.CacheSerializer; +import com.google.gerrit.server.cache.StringSerializer; import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore; import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder; import com.google.inject.TypeLiteral; @@ -124,23 +123,6 @@ public class H2CacheTest { assertThat(oldImpl.getIfPresent("key")).isNull(); } - // TODO(dborowitz): Won't be necessary when we use a real StringSerializer in the server code. - private enum StringSerializer implements CacheSerializer { - INSTANCE; - - @Override - public byte[] serialize(String object) { - return object.getBytes(UTF_8); - } - - @Override - public String deserialize(byte[] in) { - // TODO(dborowitz): Consider using CharsetDecoder directly in the real implementation, to get - // checked exceptions. - return new String(in, UTF_8); - } - } - private static Cache> disableMemCache() { return CacheBuilder.newBuilder().maximumSize(0).build(); } diff --git a/javatests/com/google/gerrit/server/git/TagSetHolderTest.java b/javatests/com/google/gerrit/server/git/TagSetHolderTest.java new file mode 100644 index 0000000000..b9bac5079e --- /dev/null +++ b/javatests/com/google/gerrit/server/git/TagSetHolderTest.java @@ -0,0 +1,57 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.git; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; + +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto; +import org.junit.Test; + +public class TagSetHolderTest { + @Test + public void serializerWithTagSet() throws Exception { + TagSetHolder holder = new TagSetHolder(new Project.NameKey("project")); + holder.setTagSet(new TagSet(holder.getProjectName())); + + byte[] serialized = TagSetHolder.Serializer.INSTANCE.serialize(holder); + assertThat(TagSetHolderProto.parseFrom(serialized)) + .ignoringRepeatedFieldOrder() + .isEqualTo( + TagSetHolderProto.newBuilder() + .setProjectName("project") + .setTags(holder.getTagSet().toProto()) + .build()); + + TagSetHolder deserialized = TagSetHolder.Serializer.INSTANCE.deserialize(serialized); + assertThat(deserialized.getProjectName()).isEqualTo(holder.getProjectName()); + TagSetTest.assertEqual(holder.getTagSet(), deserialized.getTagSet()); + } + + @Test + public void serializerWithoutTagSet() throws Exception { + TagSetHolder holder = new TagSetHolder(new Project.NameKey("project")); + + byte[] serialized = TagSetHolder.Serializer.INSTANCE.serialize(holder); + assertThat(TagSetHolderProto.parseFrom(serialized)) + .ignoringRepeatedFieldOrder() + .isEqualTo(TagSetHolderProto.newBuilder().setProjectName("project").build()); + + TagSetHolder deserialized = TagSetHolder.Serializer.INSTANCE.deserialize(serialized); + assertThat(deserialized.getProjectName()).isEqualTo(holder.getProjectName()); + TagSetTest.assertEqual(holder.getTagSet(), deserialized.getTagSet()); + } +} diff --git a/javatests/com/google/gerrit/server/git/TagSetTest.java b/javatests/com/google/gerrit/server/git/TagSetTest.java new file mode 100644 index 0000000000..2591c3d668 --- /dev/null +++ b/javatests/com/google/gerrit/server/git/TagSetTest.java @@ -0,0 +1,147 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.git; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes; + +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Streams; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto; +import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.CachedRefProto; +import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.TagProto; +import com.google.gerrit.server.git.TagSet.CachedRef; +import com.google.gerrit.server.git.TagSet.Tag; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdOwnerMap; +import org.junit.Test; + +public class TagSetTest { + @Test + public void roundTripToProto() { + HashMap refs = new HashMap<>(); + refs.put( + "refs/heads/master", + new CachedRef(1, ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))); + refs.put( + "refs/heads/branch", + new CachedRef(2, ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))); + ObjectIdOwnerMap tags = new ObjectIdOwnerMap<>(); + tags.add( + new Tag( + ObjectId.fromString("cccccccccccccccccccccccccccccccccccccccc"), newBitSet(1, 3, 5))); + tags.add( + new Tag( + ObjectId.fromString("dddddddddddddddddddddddddddddddddddddddd"), newBitSet(2, 4, 6))); + TagSet tagSet = new TagSet(new Project.NameKey("project"), refs, tags); + + TagSetProto proto = tagSet.toProto(); + assertThat(proto) + .ignoringRepeatedFieldOrder() + .isEqualTo( + TagSetProto.newBuilder() + .setProjectName("project") + .putRef( + "refs/heads/master", + CachedRefProto.newBuilder() + .setId( + bytes( + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa)) + .setFlag(1) + .build()) + .putRef( + "refs/heads/branch", + CachedRefProto.newBuilder() + .setId( + bytes( + 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb)) + .setFlag(2) + .build()) + .addTag( + TagProto.newBuilder() + .setId( + bytes( + 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, + 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc)) + .setFlags(bytes(0x2a)) + .build()) + .addTag( + TagProto.newBuilder() + .setId( + bytes( + 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, + 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd)) + .setFlags(bytes(0x54)) + .build()) + .build()); + + assertEqual(tagSet, TagSet.fromProto(proto)); + } + + // TODO(dborowitz): Find some more common place to put this method, which requires access to + // package-private TagSet details. + static void assertEqual(@Nullable TagSet a, @Nullable TagSet b) { + if (a == null || b == null) { + assertWithMessage("only one TagSet is null out of\n%s\n%s", a, b) + .that(a == null && b == null) + .isTrue(); + return; + } + assertThat(a.getProjectName()).isEqualTo(b.getProjectName()); + + Map aRefs = a.getRefsForTesting(); + Map bRefs = b.getRefsForTesting(); + assertThat(ImmutableSortedSet.copyOf(aRefs.keySet())) + .named("ref name set") + .isEqualTo(ImmutableSortedSet.copyOf(bRefs.keySet())); + for (String name : aRefs.keySet()) { + CachedRef aRef = aRefs.get(name); + CachedRef bRef = bRefs.get(name); + assertThat(aRef.get()).named("value of ref %s", name).isEqualTo(bRef.get()); + assertThat(aRef.flag).named("flag of ref %s", name).isEqualTo(bRef.flag); + } + + ObjectIdOwnerMap aTags = a.getTagsForTesting(); + ObjectIdOwnerMap bTags = b.getTagsForTesting(); + assertThat(getTagIds(aTags)).named("tag ID set").isEqualTo(getTagIds(bTags)); + for (Tag aTag : aTags) { + Tag bTag = bTags.get(aTag); + assertThat(aTag.refFlags).named("flags for tag %s", aTag.name()).isEqualTo(bTag.refFlags); + } + } + + private static ImmutableSortedSet getTagIds(ObjectIdOwnerMap bTags) { + return Streams.stream(bTags) + .map(Tag::name) + .collect(ImmutableSortedSet.toImmutableSortedSet(Comparator.naturalOrder())); + } + + private BitSet newBitSet(int... bits) { + BitSet result = new BitSet(); + Arrays.stream(bits).forEach(result::set); + return result; + } +} diff --git a/proto/cache.proto b/proto/cache.proto index a826f8cc6e..33f91434d9 100644 --- a/proto/cache.proto +++ b/proto/cache.proto @@ -193,3 +193,29 @@ message ConflictKeyProto { string submit_type = 3; bool content_merge = 4; } + +// Serialized form of com.google.gerrit.server.query.git.TagSetHolder. +// Next ID: 3 +message TagSetHolderProto { + string project_name = 1; + + // Next ID: 4 + message TagSetProto { + string project_name = 1; + + // Next ID: 3 + message CachedRefProto { + bytes id = 1; + int32 flag = 2; + } + map ref = 2; + + // Next ID: 3 + message TagProto { + bytes id = 1; + bytes flags = 2; + } + repeated TagProto tag = 3; + } + TagSetProto tags = 2; +}