Merge "Add canonical cross-browser favicon package"
diff --git a/java/com/google/gitiles/blame/cache/BlameCache.java b/java/com/google/gitiles/blame/cache/BlameCache.java
index 57a9166..7cb7bad 100644
--- a/java/com/google/gitiles/blame/cache/BlameCache.java
+++ b/java/com/google/gitiles/blame/cache/BlameCache.java
@@ -16,6 +16,7 @@
import java.io.IOException;
import java.util.List;
+import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
@@ -28,6 +29,20 @@
List<Region> get(Repository repo, ObjectId commitId, String path) throws IOException;
/**
+ * Gets the blame of a path at a given commit, ignoring the specified revisions.
+ *
+ * @return the blame of a path at a given commit.
+ */
+ default List<Region> get(
+ Repository repo, ObjectId commitId, String path, Set<ObjectId> ignoreIds)
+ throws IOException {
+ if (ignoreIds == null || ignoreIds.isEmpty()) {
+ return get(repo, commitId, path);
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ /**
* Gets the last commit that modified a path.
*
* @return the last commit that modified a path, starting at the given commit.
diff --git a/java/com/google/gitiles/blame/cache/BlameCacheImpl.java b/java/com/google/gitiles/blame/cache/BlameCacheImpl.java
index ba21aaa..1bed1c2 100644
--- a/java/com/google/gitiles/blame/cache/BlameCacheImpl.java
+++ b/java/com/google/gitiles/blame/cache/BlameCacheImpl.java
@@ -139,8 +139,15 @@
@Override
public List<Region> get(Repository repo, ObjectId commitId, String path) throws IOException {
+ return get(repo, commitId, path, ImmutableSet.of());
+ }
+
+ @Override
+ public List<Region> get(
+ Repository repo, ObjectId commitId, String path, Set<ObjectId> ignoreIds)
+ throws IOException {
try {
- Key key = new Key(commitId, path);
+ Key key = new Key(commitId, path, ignoreIds);
return cache.get(key, newLoader(key, repo));
} catch (ExecutionException e) {
throw new IOException(e);
@@ -170,6 +177,9 @@
}
try (BlameGenerator gen = new BlameGenerator(repo, key.path)) {
+ if (!key.getIgnoreIds().isEmpty()) {
+ gen.setIgnoreRevs(key.getIgnoreIds());
+ }
gen.push(null, blameCommit);
if (gen.getResultContents() == null) {
return ImmutableList.of();
diff --git a/java/com/google/gitiles/doc/SimpleMermaidRenderer.java b/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
index d4d5d56..576b72f 100644
--- a/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
+++ b/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
@@ -218,8 +218,10 @@
return isEof() ? '\0' : text.charAt(pos);
}
- char next() {
- return isEof() ? '\0' : text.charAt(pos++);
+ void advance() {
+ if (!isEof()) {
+ pos++;
+ }
}
boolean startsWith(String prefix) {
@@ -231,7 +233,19 @@
return text.substring(pos, pos + prefix.length()).equalsIgnoreCase(prefix);
}
- boolean consume(String prefix) {
+ void skip(String prefix) {
+ if (startsWith(prefix)) {
+ pos += prefix.length();
+ }
+ }
+
+ void skipIgnoreCase(String prefix) {
+ if (startsWithIgnoreCase(prefix)) {
+ pos += prefix.length();
+ }
+ }
+
+ boolean tryConsume(String prefix) {
if (startsWith(prefix)) {
pos += prefix.length();
return true;
@@ -239,7 +253,7 @@
return false;
}
- boolean consumeIgnoreCase(String prefix) {
+ boolean tryConsumeIgnoreCase(String prefix) {
if (startsWithIgnoreCase(prefix)) {
pos += prefix.length();
return true;
@@ -365,7 +379,7 @@
continue;
}
- if (s.consumeIgnoreCase("graph") || s.consumeIgnoreCase("flowchart")) {
+ if (s.tryConsumeIgnoreCase("graph") || s.tryConsumeIgnoreCase("flowchart")) {
s.skipWhitespace();
String dirStr = s.scanIdentifier().toUpperCase();
try {
@@ -434,7 +448,7 @@
continue;
}
- if (s.consumeIgnoreCase("direction")) {
+ if (s.tryConsumeIgnoreCase("direction")) {
s.skipWhitespace();
String dirStr = s.scanIdentifier().toUpperCase();
if (!subgraphStack.isEmpty() && !dirStr.isEmpty()) {
@@ -448,7 +462,7 @@
continue;
}
- if (s.consumeIgnoreCase("end")) {
+ if (s.tryConsumeIgnoreCase("end")) {
char nextC = s.peek();
if (nextC == '\0' || Character.isWhitespace(nextC) || nextC == ';') {
if (!subgraphStack.isEmpty()) {
@@ -459,7 +473,7 @@
}
}
- if (s.consumeIgnoreCase("subgraph")) {
+ if (s.tryConsumeIgnoreCase("subgraph")) {
parseSubgraphHeader(s, graph, subgraphStack);
s.skipToStatementEnd();
continue;
@@ -480,33 +494,33 @@
// Check for `subgraph "Title Only"`
if (s.startsWith("\"")) {
- s.consume("\"");
+ s.skip("\"");
int start = s.pos;
- while (!s.isEof() && !s.startsWith("\"")) s.next();
+ while (!s.isEof() && !s.startsWith("\"")) s.advance();
sgTitle = s.text.substring(start, s.pos);
- s.consume("\"");
+ s.skip("\"");
sgId = "sg_" + graph.allSubgraphs.size();
} else {
int start = s.pos;
while (!s.isEof() && !s.startsWith("[") && !s.startsWith("\"") && s.peek() != '\n' && s.peek() != ';') {
- s.next();
+ s.advance();
}
String rawName = s.text.substring(start, s.pos).trim();
s.skipWhitespace();
if (s.startsWith("[")) {
- s.consume("[");
+ s.skip("[");
s.skipWhitespace();
- boolean quoted = s.consume("\"");
+ boolean quoted = s.tryConsume("\"");
int tstart = s.pos;
if (quoted) {
- while (!s.isEof() && !s.startsWith("\"]") && !s.startsWith("\"")) s.next();
+ while (!s.isEof() && !s.startsWith("\"]") && !s.startsWith("\"")) s.advance();
sgTitle = s.text.substring(tstart, s.pos);
- s.consume("\"");
- s.consume("]");
+ s.skip("\"");
+ s.skip("]");
} else {
- while (!s.isEof() && !s.startsWith("]")) s.next();
+ while (!s.isEof() && !s.startsWith("]")) s.advance();
sgTitle = s.text.substring(tstart, s.pos);
- s.consume("]");
+ s.skip("]");
}
sgId = rawName;
} else {
@@ -532,7 +546,7 @@
}
private static void parseStyleDirective(CharScanner s, MermaidGraph graph) {
- s.consumeIgnoreCase("style");
+ s.skipIgnoreCase("style");
s.skipWhitespace();
String targetId = s.scanIdentifier();
if (targetId.isEmpty()) {
@@ -678,7 +692,7 @@
while (!s.isEof()) {
s.skipWhitespace();
if (s.startsWith("&")) {
- s.consume("&");
+ s.skip("&");
s.skipWhitespace();
RawNodeToken next = scanNodeToken(s);
if (next != null) {
@@ -781,11 +795,11 @@
String close = d[1];
String shapeName = d[2];
if (s.startsWith(open)) {
- s.consume(open);
+ s.skip(open);
shape = NodeShape.valueOf(shapeName);
s.skipWhitespace();
if (s.startsWith("\"")) {
- s.consume("\"");
+ s.skip("\"");
int start = s.pos;
while (!s.isEof() && s.peek() != '\n' && s.peek() != '\r') {
if (s.startsWith("\\\"")) {
@@ -793,20 +807,20 @@
} else if (s.startsWith("\"")) {
break;
} else {
- s.next();
+ s.advance();
}
}
label = s.text.substring(start, s.pos);
- s.consume("\"");
+ s.skip("\"");
s.skipWhitespace();
- s.consume(close);
+ s.skip(close);
} else {
int start = s.pos;
while (!s.isEof() && s.peek() != '\n' && !s.startsWith(close)) {
- s.next();
+ s.advance();
}
label = s.text.substring(start, s.pos);
- s.consume(close);
+ s.skip(close);
}
break;
}
@@ -823,43 +837,43 @@
if ((s.startsWith("-- ") || s.startsWith("--\"") || s.startsWith("--\t"))
&& !s.startsWith("-->")
&& !s.startsWith("---|")) {
- s.consume("--");
+ s.skip("--");
s.skipWhitespace();
int start = s.pos;
while (!s.isEof() && s.peek() != '\n' && !s.startsWith("-->") && !s.startsWith("---")) {
- s.next();
+ s.advance();
}
String label = cleanLabel(s.text.substring(start, s.pos));
- boolean arrow = s.consume("-->");
- if (!arrow) s.consume("---");
+ boolean arrow = s.tryConsume("-->");
+ if (!arrow) s.skip("---");
return new RawEdgeToken(EdgeStroke.SOLID, arrow, label);
}
if ((s.startsWith("== ") || s.startsWith("==\"") || s.startsWith("==\t"))
&& !s.startsWith("==>")
&& !s.startsWith("===|")) {
- s.consume("==");
+ s.skip("==");
s.skipWhitespace();
int start = s.pos;
while (!s.isEof() && s.peek() != '\n' && !s.startsWith("==>") && !s.startsWith("===")) {
- s.next();
+ s.advance();
}
String label = cleanLabel(s.text.substring(start, s.pos));
- boolean arrow = s.consume("==>");
- if (!arrow) s.consume("===");
+ boolean arrow = s.tryConsume("==>");
+ if (!arrow) s.skip("===");
return new RawEdgeToken(EdgeStroke.THICK, arrow, label);
}
if (s.startsWith("-. ") || s.startsWith("-.\"") || s.startsWith("-.\t")) {
- s.consume("-.");
+ s.skip("-.");
s.skipWhitespace();
int start = s.pos;
while (!s.isEof() && s.peek() != '\n' && !s.startsWith(".->") && !s.startsWith(".-")) {
- s.next();
+ s.advance();
}
String label = cleanLabel(s.text.substring(start, s.pos));
- boolean arrow = s.consume(".->");
- if (!arrow) s.consume(".-");
+ boolean arrow = s.tryConsume(".->");
+ if (!arrow) s.skip(".-");
return new RawEdgeToken(EdgeStroke.DASHED, arrow, label);
}
@@ -879,17 +893,17 @@
EdgeStroke stroke = EdgeStroke.valueOf(op[1]);
boolean arrow = Boolean.parseBoolean(op[2]);
if (s.startsWith(prefix)) {
- s.consume(prefix);
+ s.skip(prefix);
s.skipWhitespace();
String label = null;
if (s.startsWith("|")) {
- s.consume("|");
+ s.skip("|");
int start = s.pos;
while (!s.isEof() && s.peek() != '\n' && !s.startsWith("|")) {
- s.next();
+ s.advance();
}
label = cleanLabel(s.text.substring(start, s.pos));
- s.consume("|");
+ s.skip("|");
}
return new RawEdgeToken(stroke, arrow, label);
}
diff --git a/javatests/com/google/gitiles/blame/cache/BlameCacheTest.java b/javatests/com/google/gitiles/blame/cache/BlameCacheTest.java
index 1ead02a..10ff25d 100644
--- a/javatests/com/google/gitiles/blame/cache/BlameCacheTest.java
+++ b/javatests/com/google/gitiles/blame/cache/BlameCacheTest.java
@@ -15,16 +15,37 @@
package com.google.gitiles.blame.cache;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+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.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-/** Unit tests for {@link BlameCacheImpl.Key}. */
+/** Unit tests for {@link BlameCache} and {@link BlameCacheImpl}. */
@RunWith(JUnit4.class)
public class BlameCacheTest {
+ private TestRepository<DfsRepository> repo;
+ private BlameCacheImpl blameCache;
+
+ @Before
+ public void setUp() throws Exception {
+ repo = new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription("test")));
+ blameCache = new BlameCacheImpl();
+ }
+
@Test
public void keyEqualsAndHashCode() {
ObjectId c1 = ObjectId.fromString("1111111111111111111111111111111111111111");
@@ -64,4 +85,104 @@
+ " ignore=[2222222222222222222222222222222222222222,"
+ " 3333333333333333333333333333333333333333]");
}
+
+ @Test
+ public void getWithoutIgnoreIdsAttributesToLastCommit() throws Exception {
+ RevCommit c1 = repo.commit().add("foo.txt", "line1\nline2\n").create();
+ RevCommit c2 =
+ repo.commit().parent(c1).add("foo.txt", "line1_formatted\nline2_formatted\n").create();
+
+ List<Region> regions = blameCache.get(repo.getRepository(), c2, "foo.txt");
+ assertThat(regions).hasSize(1);
+ assertThat(regions.get(0).getSourceCommit()).isEqualTo(c2);
+ assertThat(regions.get(0).getStart()).isEqualTo(0);
+ assertThat(regions.get(0).getEnd()).isEqualTo(2);
+ }
+
+ @Test
+ public void getWithIgnoredCommitAttributesToParentCommit() throws Exception {
+ RevCommit c1 = repo.commit().add("foo.txt", "line1\nline2\n").create();
+ RevCommit c2 =
+ repo.commit().parent(c1).add("foo.txt", "line1_formatted\nline2_formatted\n").create();
+
+ List<Region> regions =
+ blameCache.get(repo.getRepository(), c2, "foo.txt", ImmutableSet.of(c2));
+ assertThat(regions).hasSize(1);
+ assertThat(regions.get(0).getSourceCommit()).isEqualTo(c1);
+ assertThat(regions.get(0).getStart()).isEqualTo(0);
+ assertThat(regions.get(0).getEnd()).isEqualTo(2);
+ }
+
+ @Test
+ public void cacheIsolationWithDifferentIgnoreIds() throws Exception {
+ RevCommit c1 = repo.commit().add("foo.txt", "line1\nline2\n").create();
+ RevCommit c2 =
+ repo.commit().parent(c1).add("foo.txt", "line1_formatted\nline2_formatted\n").create();
+
+ // Query 1: standard blame
+ List<Region> unignored = blameCache.get(repo.getRepository(), c2, "foo.txt");
+ assertThat(unignored.get(0).getSourceCommit()).isEqualTo(c2);
+
+ // Query 2: ignored blame
+ List<Region> ignored =
+ blameCache.get(repo.getRepository(), c2, "foo.txt", ImmutableSet.of(c2));
+ assertThat(ignored.get(0).getSourceCommit()).isEqualTo(c1);
+
+ // Verify both are cached under separate keys
+ assertThat(blameCache.getCache().asMap()).containsKey(new BlameCacheImpl.Key(c2, "foo.txt"));
+ assertThat(blameCache.getCache().asMap())
+ .containsKey(new BlameCacheImpl.Key(c2, "foo.txt", ImmutableSet.of(c2)));
+ assertThat(blameCache.getCache().size()).isEqualTo(2);
+ }
+
+ @Test
+ public void getWithMultipleConsecutiveIgnoredCommits() throws Exception {
+ RevCommit c1 = repo.commit().add("foo.txt", "line1\nline2\n").create();
+ RevCommit c2 =
+ repo.commit().parent(c1).add("foo.txt", "line1_format1\nline2_format1\n").create();
+ RevCommit c3 =
+ repo.commit().parent(c2).add("foo.txt", "line1_format2\nline2_format2\n").create();
+
+ // Ignore both c2 and c3 -> blame should walk back across both to c1
+ List<Region> regions =
+ blameCache.get(repo.getRepository(), c3, "foo.txt", ImmutableSet.of(c2, c3));
+ assertThat(regions).hasSize(1);
+ assertThat(regions.get(0).getSourceCommit()).isEqualTo(c1);
+ assertThat(regions.get(0).getStart()).isEqualTo(0);
+ assertThat(regions.get(0).getEnd()).isEqualTo(2);
+ }
+
+ @Test
+ public void defaultInterfaceMethodDelegatesWhenEmpty() throws Exception {
+ BlameCache customCache =
+ new BlameCache() {
+ @Override
+ public List<Region> get(Repository repo, ObjectId commitId, String path) {
+ return ImmutableList.of(new Region(null, null, null, 0, 1));
+ }
+
+ @Override
+ public ObjectId findLastCommit(Repository repo, ObjectId commitId, String path) {
+ return ObjectId.zeroId();
+ }
+ };
+
+ // When ignoreIds is null or empty, default method delegates to get(repo, commit, path)
+ List<Region> res1 = customCache.get(repo.getRepository(), ObjectId.zeroId(), "foo.txt", null);
+ assertThat(res1).hasSize(1);
+
+ List<Region> res2 =
+ customCache.get(repo.getRepository(), ObjectId.zeroId(), "foo.txt", ImmutableSet.of());
+ assertThat(res2).hasSize(1);
+
+ // When ignoreIds is non-empty, default method throws UnsupportedOperationException
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ customCache.get(
+ repo.getRepository(),
+ ObjectId.zeroId(),
+ "foo.txt",
+ ImmutableSet.of(ObjectId.zeroId())));
+ }
}
diff --git a/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java b/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java
index 6ae35c1..093b8f0 100644
--- a/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java
+++ b/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java
@@ -17,7 +17,6 @@
import static com.google.common.truth.Truth.assertThat;
import com.google.common.base.Splitter;
-import com.google.common.primitives.Doubles;
import java.util.List;
import java.util.Optional;
import org.junit.Test;
@@ -621,7 +620,7 @@
// Mutual same-layer edge with label
String code5 = "graph TD\n A --> B\n B --> A\n A -->|Golden Star| B\n C --> D\n";
- render(code5);
+ assertThat(render(code5)).isNotNull();
// Trailing non-edge characters to hit scanEdgeToken default return null
String code6 = "graph TD\n A 12345\n";
@@ -1303,10 +1302,14 @@
String d = p.getAttribute("d");
if (d.contains(" C ")) {
for (String part : Splitter.onPattern("[,\\s]+").omitEmptyStrings().split(d)) {
- Double val = Doubles.tryParse(part);
- if (val != null && val > dRight) {
- foundClearanceLoop = true;
- break;
+ try {
+ double val = Double.parseDouble(part);
+ if (val > dRight) {
+ foundClearanceLoop = true;
+ break;
+ }
+ } catch (NumberFormatException e) {
+ // Ignore non-numeric path commands (e.g. "C", "M")
}
}
}