Merge "Add canonical cross-browser favicon package"
diff --git a/java/com/google/gitiles/Renderer.java b/java/com/google/gitiles/Renderer.java
index 964063d..c8c1ba6 100644
--- a/java/com/google/gitiles/Renderer.java
+++ b/java/com/google/gitiles/Renderer.java
@@ -90,10 +90,16 @@
           "RepositoryIndex.soy");
 
   public static final ImmutableMap<String, String> STATIC_URL_GLOBALS =
-      ImmutableMap.of(
-          "gitiles.BASE_CSS_URL", "base.css",
-          "gitiles.DOC_CSS_URL", "doc.css",
-          "gitiles.PRETTIFY_CSS_URL", "prettify/prettify.css");
+      ImmutableMap.<String, String>builder()
+          .put("gitiles.BASE_CSS_URL", "base.css")
+          .put("gitiles.DOC_CSS_URL", "doc.css")
+          .put("gitiles.PRETTIFY_CSS_URL", "prettify/prettify.css")
+          .put("gitiles.FAVICON_ICO_URL", "favicon.ico")
+          .put("gitiles.FAVICON_SVG_URL", "favicon.svg")
+          .put("gitiles.FAVICON_32_URL", "favicon-32x32.png")
+          .put("gitiles.FAVICON_16_URL", "favicon-16x16.png")
+          .put("gitiles.APPLE_TOUCH_ICON_URL", "apple-touch-icon.png")
+          .build();
 
   protected static Function<String, URL> fileUrlMapper() {
     return fileUrlMapper("");
diff --git a/javatests/com/google/gitiles/FaviconTest.java b/javatests/com/google/gitiles/FaviconTest.java
new file mode 100644
index 0000000..dedcd37
--- /dev/null
+++ b/javatests/com/google/gitiles/FaviconTest.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2026 Google Inc. All Rights Reserved.
+//
+// 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.gitiles;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for favicon links and static asset mapping in Gitiles HTML pages. */
+@RunWith(JUnit4.class)
+public class FaviconTest extends ServletTest {
+
+  private void assertFaviconElementsPresent(String html) {
+    assertThat(html).contains("rel=\"icon\" href=\"/+static/favicon.ico\" sizes=\"any\"");
+    assertThat(html).contains("rel=\"icon\" type=\"image/svg+xml\"");
+    assertThat(html).contains("rel=\"icon\" type=\"image/png\" sizes=\"32x32\"");
+    assertThat(html).contains("rel=\"icon\" type=\"image/png\" sizes=\"16x16\"");
+    assertThat(html).contains("rel=\"apple-touch-icon\" sizes=\"180x180\"");
+    assertThat(html).contains("favicon.ico");
+    assertThat(html).contains("favicon.svg");
+    assertThat(html).contains("favicon-32x32.png");
+    assertThat(html).contains("favicon-16x16.png");
+    assertThat(html).contains("apple-touch-icon.png");
+  }
+
+  @Test
+  public void staticUrlGlobalsContainsAllFaviconEntries() {
+    assertThat(Renderer.STATIC_URL_GLOBALS)
+        .containsEntry("gitiles.FAVICON_ICO_URL", "favicon.ico");
+    assertThat(Renderer.STATIC_URL_GLOBALS)
+        .containsEntry("gitiles.FAVICON_SVG_URL", "favicon.svg");
+    assertThat(Renderer.STATIC_URL_GLOBALS)
+        .containsEntry("gitiles.FAVICON_32_URL", "favicon-32x32.png");
+    assertThat(Renderer.STATIC_URL_GLOBALS)
+        .containsEntry("gitiles.FAVICON_16_URL", "favicon-16x16.png");
+    assertThat(Renderer.STATIC_URL_GLOBALS)
+        .containsEntry("gitiles.APPLE_TOUCH_ICON_URL", "apple-touch-icon.png");
+  }
+
+  @Test
+  public void hostIndexIncludesFavicons() throws Exception {
+    String html = buildHtml("/", false);
+    assertFaviconElementsPresent(html);
+  }
+
+  @Test
+  public void repositoryIndexIncludesFavicons() throws Exception {
+    String html = buildHtml("/repo/", false);
+    assertFaviconElementsPresent(html);
+  }
+
+  @Test
+  public void docPageIncludesFavicons() throws Exception {
+    repo.branch("master")
+        .commit()
+        .add("README.md", "# Test Document\n\nDoc content.")
+        .create();
+
+    String html = buildHtml("/repo/+doc/master/README.md", false);
+    assertFaviconElementsPresent(html);
+  }
+
+  @Test
+  public void commitDetailIncludesFavicons() throws Exception {
+    RevCommit commit = repo.branch("master").commit().add("foo", "contents").create();
+    String html = buildHtml("/repo/+/" + commit.name(), false);
+    assertFaviconElementsPresent(html);
+  }
+}
diff --git a/resources/com/google/gitiles/static/android-chrome-192x192.png b/resources/com/google/gitiles/static/android-chrome-192x192.png
new file mode 100644
index 0000000..4ec6f19
--- /dev/null
+++ b/resources/com/google/gitiles/static/android-chrome-192x192.png
Binary files differ
diff --git a/resources/com/google/gitiles/static/android-chrome-512x512.png b/resources/com/google/gitiles/static/android-chrome-512x512.png
new file mode 100644
index 0000000..88dbe2e
--- /dev/null
+++ b/resources/com/google/gitiles/static/android-chrome-512x512.png
Binary files differ
diff --git a/resources/com/google/gitiles/static/apple-touch-icon.png b/resources/com/google/gitiles/static/apple-touch-icon.png
new file mode 100644
index 0000000..b4c1032
--- /dev/null
+++ b/resources/com/google/gitiles/static/apple-touch-icon.png
Binary files differ
diff --git a/resources/com/google/gitiles/static/favicon-16x16.png b/resources/com/google/gitiles/static/favicon-16x16.png
new file mode 100644
index 0000000..422d3f7
--- /dev/null
+++ b/resources/com/google/gitiles/static/favicon-16x16.png
Binary files differ
diff --git a/resources/com/google/gitiles/static/favicon-32x32.png b/resources/com/google/gitiles/static/favicon-32x32.png
new file mode 100644
index 0000000..62cc3fb
--- /dev/null
+++ b/resources/com/google/gitiles/static/favicon-32x32.png
Binary files differ
diff --git a/resources/com/google/gitiles/static/favicon.ico b/resources/com/google/gitiles/static/favicon.ico
new file mode 100644
index 0000000..fe69fb6
--- /dev/null
+++ b/resources/com/google/gitiles/static/favicon.ico
Binary files differ
diff --git a/resources/com/google/gitiles/static/favicon.svg b/resources/com/google/gitiles/static/favicon.svg
new file mode 100644
index 0000000..249a0d0
--- /dev/null
+++ b/resources/com/google/gitiles/static/favicon.svg
@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
+  <!-- Background Squircle in Git Coral-Orange -->
+  <rect width="512" height="512" rx="112" ry="112" fill="#F05133"/>
+
+  <!-- UNIFIED WHITE Y-FORK (Exact flush geometry, 40px uniform width, zero bulge) -->
+  <polygon points="236,366 236,257 139,175 165,145 256,222 347,145 373,175 276,257 276,366" fill="#FFFFFF"/>
+
+  <!-- THREE COMMIT NODE RINGS (Outer White Circles) -->
+  <circle cx="256" cy="366" r="62" fill="#FFFFFF"/>
+  <circle cx="152" cy="160" r="62" fill="#FFFFFF"/>
+  <circle cx="360" cy="160" r="62" fill="#FFFFFF"/>
+
+  <!-- INNER HOLLOW DONUT CORES (Punched out in Coral-Orange) -->
+  <circle cx="256" cy="366" r="32" fill="#F05133"/>
+  <circle cx="152" cy="160" r="32" fill="#F05133"/>
+  <circle cx="360" cy="160" r="32" fill="#F05133"/>
+</svg>
diff --git a/resources/com/google/gitiles/templates/Common.soy b/resources/com/google/gitiles/templates/Common.soy
index 371a10d..0aca3dc 100644
--- a/resources/com/google/gitiles/templates/Common.soy
+++ b/resources/com/google/gitiles/templates/Common.soy
@@ -43,6 +43,11 @@
     {/msg}
   </title>
 
+  <link rel="icon" href="{$staticUrls.FAVICON_ICO_URL}" sizes="any">
+  <link rel="icon" type="image/svg+xml" href="{$staticUrls.FAVICON_SVG_URL}">
+  <link rel="icon" type="image/png" sizes="32x32" href="{$staticUrls.FAVICON_32_URL}">
+  <link rel="icon" type="image/png" sizes="16x16" href="{$staticUrls.FAVICON_16_URL}">
+  <link rel="apple-touch-icon" sizes="180x180" href="{$staticUrls.APPLE_TOUCH_ICON_URL}">
   <link rel="stylesheet" type="text/css" href="{$staticUrls.BASE_CSS_URL}">
   {if $css && length($css)}
     {for $url in $css}
diff --git a/resources/com/google/gitiles/templates/Doc.soy b/resources/com/google/gitiles/templates/Doc.soy
index 36bf1d5..fab786b 100644
--- a/resources/com/google/gitiles/templates/Doc.soy
+++ b/resources/com/google/gitiles/templates/Doc.soy
@@ -59,6 +59,11 @@
     {if $siteTitle}{$siteTitle} -{sp}{/if}
     {$pageTitle}
   </title>
+  <link rel="icon" href="{$staticUrls.FAVICON_ICO_URL}" sizes="any" />
+  <link rel="icon" type="image/svg+xml" href="{$staticUrls.FAVICON_SVG_URL}" />
+  <link rel="icon" type="image/png" sizes="32x32" href="{$staticUrls.FAVICON_32_URL}" />
+  <link rel="icon" type="image/png" sizes="16x16" href="{$staticUrls.FAVICON_16_URL}" />
+  <link rel="apple-touch-icon" sizes="180x180" href="{$staticUrls.APPLE_TOUCH_ICON_URL}" />
   <link rel="stylesheet" type="text/css" href="{$staticUrls.BASE_CSS_URL}" />
   <link rel="stylesheet" type="text/css" href="{$staticUrls.DOC_CSS_URL}" />
   <link rel="stylesheet" type="text/css" href="{$staticUrls.PRETTIFY_CSS_URL}" />