Merge "Cache sorted ref lists in CommitData.getRefsById"
diff --git a/Documentation/config.md b/Documentation/config.md
index 29f328f..c9c7448 100644
--- a/Documentation/config.md
+++ b/Documentation/config.md
@@ -186,6 +186,10 @@
 * `blocknote`: Gitiles style note/promo/aside blocks to raise
   awareness to important content. Default false.
 
+* `frontmatter`: strip a leading YAML front matter block (delimited
+  by `---` lines) from the rendered output instead of treating it as
+  document content. Default false.
+
 * `ghthematicbreak`: accept `--` for `<hr>`, like GitHub Flavor
   Markdown.  Default follows `githubFlavor`.
 
diff --git a/Documentation/markdown.md b/Documentation/markdown.md
index 4c76388..83f14d7 100644
--- a/Documentation/markdown.md
+++ b/Documentation/markdown.md
@@ -474,6 +474,26 @@
 Gitiles includes additional extensions to the Markdown language that
 make documentation writing for the web easier without using raw HTML.
 
+### Front matter
+
+Requires `markdown.frontmatter` to be true.
+
+Files authored for static site generators (such as Jekyll or Hugo)
+often begin with a YAML *front matter* block delimited by `---` lines:
+
+```
+---
+title: My page
+layout: post
+---
+
+# My page
+```
+
+When enabled, Gitiles strips this leading block from the rendered
+output instead of displaying the raw `key: value` metadata as
+document content. The block must begin on the first line of the file.
+
 ### Table of contents
 
 Requires `markdown.toc` to be true.
diff --git a/external_deps.lock.json b/external_deps.lock.json
index 3410784..36df3f9 100755
--- a/external_deps.lock.json
+++ b/external_deps.lock.json
@@ -23,8 +23,9 @@
     "com.jcraft:jzlib": 864660349,
     "commons-codec:commons-codec": -1724312797,
     "commons-io:commons-io": -1271816170,
-    "commons-logging:commons-logging": -1374531806,
+    "commons-logging:commons-logging": -916690979,
     "io.github.java-diff-utils:java-diff-utils": 1296992014,
+    "jakarta.servlet:jakarta.servlet-api": -3550902,
     "javax.servlet:javax.servlet-api": 669233360,
     "junit:junit": -744267592,
     "net.bytebuddy:byte-buddy": 1769080559,
@@ -49,6 +50,8 @@
     "org.commonmark:commonmark-ext-autolink": -1853742120,
     "org.commonmark:commonmark-ext-gfm-strikethrough": 350394231,
     "org.commonmark:commonmark-ext-gfm-tables": 1881582931,
+    "org.commonmark:commonmark-ext-yaml-front-matter": -1519651186,
+    "org.eclipse.jetty.ee10:jetty-ee10-servlet": 835495267,
     "org.eclipse.jetty.ee8:jetty-ee8-nested": 1867536598,
     "org.eclipse.jetty.ee8:jetty-ee8-security": -374913491,
     "org.eclipse.jetty.ee8:jetty-ee8-servlet": 894276934,
@@ -134,12 +137,14 @@
     "commons-codec:commons-codec:jar:sources": -499809306,
     "commons-io:commons-io": 1539755249,
     "commons-io:commons-io:jar:sources": 614234247,
-    "commons-logging:commons-logging": -102005530,
-    "commons-logging:commons-logging:jar:sources": 147282524,
+    "commons-logging:commons-logging": -2039356713,
+    "commons-logging:commons-logging:jar:sources": 494659498,
     "io.github.java-diff-utils:java-diff-utils": -1180802694,
     "io.github.java-diff-utils:java-diff-utils:jar:sources": 1551226070,
     "jakarta.inject:jakarta.inject-api": 188991469,
     "jakarta.inject:jakarta.inject-api:jar:sources": -1584622703,
+    "jakarta.servlet:jakarta.servlet-api": -1421863120,
+    "jakarta.servlet:jakarta.servlet-api:jar:sources": -2084357823,
     "javax.activation:activation": -1387662613,
     "javax.activation:activation:jar:sources": -848464236,
     "javax.annotation:jsr250-api": 847275234,
@@ -173,7 +178,7 @@
     "org.apache.commons:commons-math3:jar:sources": -2132756896,
     "org.apache.commons:commons-text": 1458185425,
     "org.apache.commons:commons-text:jar:sources": -596520490,
-    "org.apache.httpcomponents:httpclient": 726829008,
+    "org.apache.httpcomponents:httpclient": -569179945,
     "org.apache.httpcomponents:httpclient:jar:sources": -779528763,
     "org.apache.httpcomponents:httpcore": 67453319,
     "org.apache.httpcomponents:httpcore:jar:sources": 2134002822,
@@ -204,7 +209,11 @@
     "org.commonmark:commonmark-ext-gfm-strikethrough:jar:sources": 992870423,
     "org.commonmark:commonmark-ext-gfm-tables": -1205584749,
     "org.commonmark:commonmark-ext-gfm-tables:jar:sources": 1341057091,
+    "org.commonmark:commonmark-ext-yaml-front-matter": 1918089254,
+    "org.commonmark:commonmark-ext-yaml-front-matter:jar:sources": -902462472,
     "org.commonmark:commonmark:jar:sources": -1511261547,
+    "org.eclipse.jetty.ee10:jetty-ee10-servlet": 349394374,
+    "org.eclipse.jetty.ee10:jetty-ee10-servlet:jar:sources": 501419503,
     "org.eclipse.jetty.ee8:jetty-ee8-nested": -65830655,
     "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources": -2141029892,
     "org.eclipse.jetty.ee8:jetty-ee8-security": -1787377969,
@@ -474,10 +483,10 @@
     },
     "commons-logging:commons-logging": {
       "shasums": {
-        "jar": "f8ead8943401081dea0aa824b5b1ba40a0e4ed297a572a0f02258150a0b62357",
-        "sources": "6e821e03cfc64e509cc162d428af84697b6c4188eb14c0b137c75b69649976ef"
+        "jar": "d175dbd751dd782a63bde28c7a039520e971f25e84b79c19b8435edc3603e0dc",
+        "sources": "28104e5057a11c6e9cda72e973d767b3a0185e65363fede91bc848bc6a309634"
       },
-      "version": "1.3.6"
+      "version": "1.4.0"
     },
     "io.github.java-diff-utils:java-diff-utils": {
       "shasums": {
@@ -493,6 +502,13 @@
       },
       "version": "2.0.1"
     },
+    "jakarta.servlet:jakarta.servlet-api": {
+      "shasums": {
+        "jar": "8a31f465f3593bf2351531a5c952014eb839da96a605b5825b93dd54714c48c4",
+        "sources": "6eb958543e0548bb93d2519e40224d13c8003b10cc615b5652bfd9899350bfb4"
+      },
+      "version": "6.1.0"
+    },
     "javax.activation:activation": {
       "shasums": {
         "jar": "2881c79c9d6ef01c58e62beea13e9d1ac8b8baa16f2fc198ad6e6776defdcdd3",
@@ -723,6 +739,20 @@
       },
       "version": "0.24.0"
     },
+    "org.commonmark:commonmark-ext-yaml-front-matter": {
+      "shasums": {
+        "jar": "a845baba681ccbf385695fbaa6d58eb40d5ecc68f3edb968c42f074f630e8fec",
+        "sources": "5cee64663842f1e7128af9c3e62ef183ea2cf205436d27a08b7cb5be0b20edb6"
+      },
+      "version": "0.24.0"
+    },
+    "org.eclipse.jetty.ee10:jetty-ee10-servlet": {
+      "shasums": {
+        "jar": "05cccc74bbe1478ceb765e967b1cb1a3c8d9b51e99acf586190887e467ffd87d",
+        "sources": "51a734b4bc04235b24ccd759515dd2947e5b675c227c556f02262b765bce47eb"
+      },
+      "version": "12.1.10"
+    },
     "org.eclipse.jetty.ee8:jetty-ee8-nested": {
       "shasums": {
         "jar": "240df6fb5bb28545183cbac87f191c0d21074b17a2c9d4562581a68a8d79c347",
@@ -1068,6 +1098,16 @@
     "org.commonmark:commonmark-ext-gfm-tables": [
       "org.commonmark:commonmark"
     ],
+    "org.commonmark:commonmark-ext-yaml-front-matter": [
+      "org.commonmark:commonmark"
+    ],
+    "org.eclipse.jetty.ee10:jetty-ee10-servlet": [
+      "jakarta.servlet:jakarta.servlet-api",
+      "org.eclipse.jetty:jetty-security",
+      "org.eclipse.jetty:jetty-server",
+      "org.eclipse.jetty:jetty-session",
+      "org.slf4j:slf4j-api"
+    ],
     "org.eclipse.jetty.ee8:jetty-ee8-nested": [
       "org.eclipse.jetty.toolchain:jetty-servlet-api",
       "org.eclipse.jetty:jetty-http",
@@ -1414,7 +1454,8 @@
     ],
     "commons-logging:commons-logging": [
       "org.apache.commons.logging",
-      "org.apache.commons.logging.impl"
+      "org.apache.commons.logging.impl",
+      "org.apache.commons.logging.jakarta"
     ],
     "io.github.java-diff-utils:java-diff-utils": [
       "com.github.difflib",
@@ -1428,6 +1469,12 @@
     "jakarta.inject:jakarta.inject-api": [
       "jakarta.inject"
     ],
+    "jakarta.servlet:jakarta.servlet-api": [
+      "jakarta.servlet",
+      "jakarta.servlet.annotation",
+      "jakarta.servlet.descriptor",
+      "jakarta.servlet.http"
+    ],
     "javax.activation:activation": [
       "com.sun.activation.registries",
       "com.sun.activation.viewers",
@@ -2413,6 +2460,19 @@
       "org.commonmark.ext.gfm.tables",
       "org.commonmark.ext.gfm.tables.internal"
     ],
+    "org.commonmark:commonmark-ext-yaml-front-matter": [
+      "org.commonmark.ext.front.matter",
+      "org.commonmark.ext.front.matter.internal"
+    ],
+    "org.eclipse.jetty.ee10:jetty-ee10-servlet": [
+      "org.eclipse.jetty.ee10.servlet",
+      "org.eclipse.jetty.ee10.servlet.internal",
+      "org.eclipse.jetty.ee10.servlet.jmx",
+      "org.eclipse.jetty.ee10.servlet.listener",
+      "org.eclipse.jetty.ee10.servlet.security",
+      "org.eclipse.jetty.ee10.servlet.security.authentication",
+      "org.eclipse.jetty.ee10.servlet.util"
+    ],
     "org.eclipse.jetty.ee8:jetty-ee8-nested": [
       "org.eclipse.jetty.ee8.nested",
       "org.eclipse.jetty.ee8.nested.jmx"
@@ -2711,6 +2771,8 @@
       "io.github.java-diff-utils:java-diff-utils:jar:sources",
       "jakarta.inject:jakarta.inject-api",
       "jakarta.inject:jakarta.inject-api:jar:sources",
+      "jakarta.servlet:jakarta.servlet-api",
+      "jakarta.servlet:jakarta.servlet-api:jar:sources",
       "javax.activation:activation",
       "javax.activation:activation:jar:sources",
       "javax.annotation:jsr250-api",
@@ -2774,7 +2836,11 @@
       "org.commonmark:commonmark-ext-gfm-strikethrough:jar:sources",
       "org.commonmark:commonmark-ext-gfm-tables",
       "org.commonmark:commonmark-ext-gfm-tables:jar:sources",
+      "org.commonmark:commonmark-ext-yaml-front-matter",
+      "org.commonmark:commonmark-ext-yaml-front-matter:jar:sources",
       "org.commonmark:commonmark:jar:sources",
+      "org.eclipse.jetty.ee10:jetty-ee10-servlet",
+      "org.eclipse.jetty.ee10:jetty-ee10-servlet:jar:sources",
       "org.eclipse.jetty.ee8:jetty-ee8-nested",
       "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources",
       "org.eclipse.jetty.ee8:jetty-ee8-security",
diff --git a/java/com/google/gitiles/BUILD b/java/com/google/gitiles/BUILD
index 84e8166..404f413 100644
--- a/java/com/google/gitiles/BUILD
+++ b/java/com/google/gitiles/BUILD
@@ -6,6 +6,7 @@
     "//java/com/google/gitiles/blame/cache",
     "//lib:autolink",
     "//lib:cm-autolink",
+    "//lib:cm-yaml-front-matter",
     "//lib:commonmark",
     "//lib:commons-io",
     "//lib:commons-lang3",
diff --git a/java/com/google/gitiles/doc/GitilesMarkdown.java b/java/com/google/gitiles/doc/GitilesMarkdown.java
index e094931..f4c7d6e 100644
--- a/java/com/google/gitiles/doc/GitilesMarkdown.java
+++ b/java/com/google/gitiles/doc/GitilesMarkdown.java
@@ -18,6 +18,7 @@
 import java.util.List;
 import org.commonmark.Extension;
 import org.commonmark.ext.autolink.AutolinkExtension;
+import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
 import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
 import org.commonmark.ext.gfm.tables.TablesExtension;
 import org.commonmark.node.Node;
@@ -38,6 +39,9 @@
     if (cfg.blockNote) {
       ext.add(BlockNoteExtension.create());
     }
+    if (cfg.frontMatter) {
+      ext.add(YamlFrontMatterExtension.create());
+    }
     if (cfg.safeHtml) {
       ext.add(GitilesHtmlExtension.create());
     }
diff --git a/java/com/google/gitiles/doc/MarkdownConfig.java b/java/com/google/gitiles/doc/MarkdownConfig.java
index 4758654..53d979a 100644
--- a/java/com/google/gitiles/doc/MarkdownConfig.java
+++ b/java/com/google/gitiles/doc/MarkdownConfig.java
@@ -44,6 +44,7 @@
 
   final boolean autoLink;
   final boolean blockNote;
+  final boolean frontMatter;
   final boolean ghThematicBreak;
   final boolean multiColumn;
   final boolean namedAnchor;
@@ -65,6 +66,7 @@
     boolean githubFlavor = cfg.getBoolean("markdown", "githubFlavor", true);
     autoLink = cfg.getBoolean("markdown", "autolink", githubFlavor);
     blockNote = cfg.getBoolean("markdown", "blocknote", false);
+    frontMatter = cfg.getBoolean("markdown", "frontmatter", false);
     ghThematicBreak = cfg.getBoolean("markdown", "ghthematicbreak", githubFlavor);
     multiColumn = cfg.getBoolean("markdown", "multicolumn", false);
     namedAnchor = cfg.getBoolean("markdown", "namedanchor", false);
@@ -94,6 +96,7 @@
 
     autoLink = on("autolink", p.autoLink, enable, disable);
     blockNote = on("blocknote", p.blockNote, enable, disable);
+    frontMatter = on("frontmatter", p.frontMatter, enable, disable);
     ghThematicBreak = on("ghthematicbreak", p.ghThematicBreak, enable, disable);
     multiColumn = on("multicolumn", p.multiColumn, enable, disable);
     namedAnchor = on("namedanchor", p.namedAnchor, enable, disable);
diff --git a/java/com/google/gitiles/doc/MarkdownToHtml.java b/java/com/google/gitiles/doc/MarkdownToHtml.java
index cb8afae..f0518e6 100644
--- a/java/com/google/gitiles/doc/MarkdownToHtml.java
+++ b/java/com/google/gitiles/doc/MarkdownToHtml.java
@@ -27,6 +27,7 @@
 import java.util.Collections;
 import java.util.List;
 import javax.annotation.Nullable;
+import org.commonmark.ext.front.matter.YamlFrontMatterBlock;
 import org.commonmark.ext.gfm.strikethrough.Strikethrough;
 import org.commonmark.ext.gfm.tables.TableBlock;
 import org.commonmark.ext.gfm.tables.TableBody;
@@ -576,6 +577,10 @@
       visit((TableBlock) node);
     } else if (node instanceof TocBlock) {
       toc.format();
+    } else if (node instanceof YamlFrontMatterBlock) {
+      // YAML front matter is document metadata: omit the whole block. We
+      // intentionally do not recurse into it, so its YamlFrontMatterNode
+      // children are never visited and need no visit(CustomNode) handling.
     } else {
       throw new IllegalArgumentException("cannot render " + node.getClass());
     }
diff --git a/javatests/com/google/gitiles/BUILD b/javatests/com/google/gitiles/BUILD
index 692537d..b2410ff 100644
--- a/javatests/com/google/gitiles/BUILD
+++ b/javatests/com/google/gitiles/BUILD
@@ -42,8 +42,10 @@
         "//java/com/google/gitiles:servlet",
         ":testutil",
         "//lib:servlet-api",
+        "//lib:commonmark",
         "//lib:commons-codec",
         "//lib:commons-io",
+        "//lib:html-types",
         "//lib/truth",
         "//lib:jgit-junit",
         "//lib/junit",
diff --git a/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java b/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java
new file mode 100644
index 0000000..74e4c3e
--- /dev/null
+++ b/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2026 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.gitiles.doc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.html.types.SafeHtml;
+import com.google.gitiles.GitilesView;
+import org.commonmark.node.Node;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for YAML front matter handling in {@link GitilesMarkdown}. */
+@RunWith(JUnit4.class)
+public class GitilesMarkdownTest {
+  private static final String MARKDOWN =
+      "---\ntitle: Kittens\nlayout: post\n---\n\n# Heading\n\nBody text.\n";
+
+  @Test
+  public void frontMatterStrippedWhenEnabled() {
+    String html = render(MARKDOWN, /* frontMatter= */ true);
+    assertThat(html).doesNotContain("title: Kittens");
+    assertThat(html).doesNotContain("layout: post");
+    assertThat(html).contains("Heading</h1>");
+    assertThat(html).contains("Body text.");
+  }
+
+  @Test
+  public void frontMatterNodeChildrenAreStrippedWithoutError() {
+    // Front matter parses into a YamlFrontMatterBlock whose YamlFrontMatterNode
+    // (CustomNode) children hold the key/value data, including list values.
+    // visit(CustomBlock) omits the block without recursing, so those children
+    // are never visited: rendering neither throws (as it would for an unhandled
+    // CustomNode) nor leaks the metadata.
+    String markdown =
+        "---\n"
+            + "title: Kittens\n"
+            + "tags:\n"
+            + "  - cats\n"
+            + "  - fluffy\n"
+            + "---\n"
+            + "\n"
+            + "# Heading\n"
+            + "\n"
+            + "Body text.\n";
+    String html = render(markdown, /* frontMatter= */ true);
+    assertThat(html).doesNotContain("Kittens");
+    assertThat(html).doesNotContain("cats");
+    assertThat(html).doesNotContain("fluffy");
+    assertThat(html).contains("Heading</h1>");
+    assertThat(html).contains("Body text.");
+  }
+
+  @Test
+  public void frontMatterRenderedAsContentWhenDisabled() {
+    // Without the extension the delimiters and keys are parsed as ordinary
+    // Markdown, leaking the raw metadata into the output. This is the behavior
+    // markdown.frontmatter is meant to fix.
+    String html = render(MARKDOWN, /* frontMatter= */ false);
+    assertThat(html).contains("title: Kittens");
+  }
+
+  private static String render(String markdown, boolean frontMatter) {
+    Config cfg = new Config();
+    cfg.setBoolean("markdown", null, "frontmatter", frontMatter);
+    MarkdownConfig mc = new MarkdownConfig(cfg);
+    Node node = GitilesMarkdown.parse(mc, markdown);
+    GitilesView view =
+        GitilesView.revision()
+            .setHostName("127.0.0.1")
+            .setServletPath("/g")
+            .setRepositoryName("repo")
+            .setRevision("HEAD")
+            .build();
+    SafeHtml html =
+        MarkdownToHtml.builder()
+            .setGitilesView(view)
+            .setConfig(mc)
+            .setFilePath("index.md")
+            .build()
+            .toSoyHtml(node);
+    return html == null ? "" : html.getSafeHtmlString();
+  }
+}
diff --git a/lib/BUILD b/lib/BUILD
index a36e385..bd8b90a 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -17,6 +17,7 @@
     ("cm-autolink", "@external_deps//:org_commonmark_commonmark_ext_autolink"),
     ("gfm-strikethrough", "@external_deps//:org_commonmark_commonmark_ext_gfm_strikethrough"),
     ("gfm-tables", "@external_deps//:org_commonmark_commonmark_ext_gfm_tables"),
+    ("cm-yaml-front-matter", "@external_deps//:org_commonmark_commonmark_ext_yaml_front_matter"),
     ("html-types", "@external_deps//:com_google_common_html_types_types"),
     ("jsr305", "@external_deps//:com_google_code_findbugs_jsr305"),
     ("servlet-api", "@external_deps//:javax_servlet_javax_servlet_api"),
@@ -77,7 +78,7 @@
 java_library(
     name = "jgit-servlet",
     visibility = ["//visibility:public"],
-    exports = ["@jgit//org.eclipse.jgit.http.server:jgit-servlet"],
+    exports = ["@jgit//org.eclipse.jgit.http.server.ee8:jgit-servlet-ee8"],
     runtime_deps = [":jgit"],
 )
 
diff --git a/modules/jgit b/modules/jgit
index c89601f..200be16 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit c89601f3395d6128a681d62b8986f42197c39a4f
+Subproject commit 200be16661d3b9983bd34affd8e424daf5ad23d7
diff --git a/tools/java_deps.MODULE.bazel b/tools/java_deps.MODULE.bazel
index 77836a2..5a5a4e9 100644
--- a/tools/java_deps.MODULE.bazel
+++ b/tools/java_deps.MODULE.bazel
@@ -101,6 +101,7 @@
         "org.commonmark:commonmark-ext-autolink:" + COMMONMARK_VERSION,
         "org.commonmark:commonmark-ext-gfm-strikethrough:" + COMMONMARK_VERSION,
         "org.commonmark:commonmark-ext-gfm-tables:" + COMMONMARK_VERSION,
+        "org.commonmark:commonmark-ext-yaml-front-matter:" + COMMONMARK_VERSION,
         "org.eclipse.jetty:jetty-http:" + JETTY_VERSION,
         "org.eclipse.jetty:jetty-io:" + JETTY_VERSION,
         "org.eclipse.jetty:jetty-security:" + JETTY_VERSION,