Merge "Avoid scanning every ref to render the commit log"
diff --git a/java/com/google/gitiles/CommitData.java b/java/com/google/gitiles/CommitData.java
index 203051b..25797d2 100644
--- a/java/com/google/gitiles/CommitData.java
+++ b/java/com/google/gitiles/CommitData.java
@@ -25,6 +25,8 @@
import com.google.common.collect.Sets;
import java.io.IOException;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -40,6 +42,7 @@
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -84,7 +87,7 @@
static class Builder {
private ArchiveFormat archiveFormat;
- private Map<AnyObjectId, Set<Ref>> refsById;
+ private Map<String, Map<AnyObjectId, Set<Ref>>> refsByIdByPrefix;
private static final int MAX_NOTE_SIZE = 524288;
Builder setArchiveFormat(@Nullable ArchiveFormat archiveFormat) {
@@ -204,17 +207,38 @@
}
private List<Ref> getRefsById(Repository repo, ObjectId id, String prefix) throws IOException {
+ if (refsByIdByPrefix == null) {
+ refsByIdByPrefix = new HashMap<>();
+ }
+ Map<AnyObjectId, Set<Ref>> refsById = refsByIdByPrefix.get(prefix);
if (refsById == null) {
- refsById = repo.getAllRefsByPeeledObjectId();
+ // Index only refs under this namespace. getAllRefsByPeeledObjectId peels every ref, which
+ // is catastrophic on repos with many refs (e.g. Gerrit's refs/changes/*).
+ refsById = new HashMap<>();
+ RefDatabase refDb = repo.getRefDatabase();
+ for (Ref ref : refDb.getRefsByPrefix(prefix)) {
+ ObjectId target;
+ try {
+ target = refDb.peel(ref).getPeeledObjectId();
+ } catch (IOException e) {
+ // Treat an unpeelable ref (e.g. missing target) as unpeeled rather than failing the
+ // whole page, like Repository#peel.
+ target = null;
+ }
+ if (target == null) {
+ target = ref.getObjectId();
+ }
+ if (target != null) {
+ refsById.computeIfAbsent(target, k -> new HashSet<>()).add(ref);
+ }
+ }
+ refsByIdByPrefix.put(prefix, refsById);
}
Set<Ref> refs = refsById.get(id);
if (refs == null) {
return ImmutableList.of();
}
- return refs.stream()
- .filter(r -> r.getName().startsWith(prefix))
- .sorted(comparing(Ref::getName))
- .collect(toList());
+ return refs.stream().sorted(comparing(Ref::getName)).collect(toList());
}
private AbstractTreeIterator getTreeIterator(RevWalk walk, RevCommit commit)
diff --git a/javatests/com/google/gitiles/LogServletTest.java b/javatests/com/google/gitiles/LogServletTest.java
index 2916f3a..dcbf817 100644
--- a/javatests/com/google/gitiles/LogServletTest.java
+++ b/javatests/com/google/gitiles/LogServletTest.java
@@ -21,12 +21,22 @@
import com.google.gitiles.CommitJsonData.Log;
import com.google.gitiles.DateFormatter.Format;
import com.google.gson.reflect.TypeToken;
+import java.io.IOException;
import java.util.ArrayList;
+import java.util.Map;
+import java.util.Set;
import org.eclipse.jgit.internal.storage.commitgraph.ChangedPathFilter;
import org.eclipse.jgit.internal.storage.dfs.DfsGarbageCollector;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Test;
@@ -445,6 +455,74 @@
assertThat(filter2).isNotNull();
}
+ @Test
+ public void logHtmlDecorationDoesNotEnumerateAllRefs() throws Exception {
+ RefCountingRepository counter = new RefCountingRepository(new DfsRepositoryDescription("repo"));
+ TestRepository<DfsRepository> r = newRepoWithManyRefs(counter);
+ GitilesServlet s = TestGitilesServlet.create(r);
+
+ FakeHttpServletResponse res = renderHtmlLog(s, "/repo/+log/refs/heads/master");
+
+ assertThat(res.getStatus()).isEqualTo(SC_OK);
+ assertThat(counter.allRefsByPeeledObjectIdCalls).isEqualTo(0);
+ }
+
+ @Test
+ public void logHtmlShowsBranchAndAnnotatedTagLabels() throws Exception {
+ RefCountingRepository counter = new RefCountingRepository(new DfsRepositoryDescription("repo"));
+ TestRepository<DfsRepository> r = newRepoWithManyRefs(counter);
+ GitilesServlet s = TestGitilesServlet.create(r);
+
+ FakeHttpServletResponse res = renderHtmlLog(s, "/repo/+log/refs/heads/master");
+
+ assertThat(res.getStatus()).isEqualTo(SC_OK);
+ String body = res.getActualBodyString();
+ assertThat(body).contains("CommitLog-branchLabel");
+ // Tag label only appears if the annotated tag was peeled to its commit; guards peeling.
+ assertThat(body).contains("CommitLog-tagLabel");
+ }
+
+ /**
+ * Builds a repo whose tip has a branch and an annotated tag, plus many unrelated refs (as a
+ * Gerrit repo would have under refs/changes/*) that must be irrelevant to log decoration.
+ */
+ private static TestRepository<DfsRepository> newRepoWithManyRefs(RefCountingRepository counter)
+ throws Exception {
+ TestRepository<DfsRepository> r =
+ new TestRepository<>(counter, new RevWalk(counter), new MockSystemReader());
+ RevCommit tip = r.branch("refs/heads/master").commit().add("foo", "contents").create();
+ r.update("refs/tags/v1", r.tag("v1", tip));
+ for (int i = 0; i < 500; i++) {
+ r.update("refs/changes/" + (i % 100) + "/" + i + "/1", tip);
+ }
+ return r;
+ }
+
+ private static FakeHttpServletResponse renderHtmlLog(GitilesServlet servlet, String path)
+ throws Exception {
+ FakeHttpServletRequest req = FakeHttpServletRequest.newRequest();
+ req.setPathInfo(path);
+ req.setQueryString("format=html");
+ FakeHttpServletResponse res = new FakeHttpServletResponse();
+ servlet.service(req, res);
+ return res;
+ }
+
+ /** {@link InMemoryRepository} that counts calls to the whole-repo ref enumeration helper. */
+ private static final class RefCountingRepository extends InMemoryRepository {
+ int allRefsByPeeledObjectIdCalls;
+
+ RefCountingRepository(DfsRepositoryDescription description) {
+ super(description);
+ }
+
+ @Override
+ public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() throws IOException {
+ allRefsByPeeledObjectIdCalls++;
+ return super.getAllRefsByPeeledObjectId();
+ }
+ }
+
private void testPrettyHtmlOutput(
String prettyType, boolean shouldShowAuthor, boolean shouldShowCommitter) throws Exception {
RevCommit parent = repo.branch(MAIN).commit().add("foo", "contents").create();