GrepServlet: add HTML view for content search The +grep endpoint currently supports JSON responses only. Add a basic Soy-backed HTML page with a search form and linked file/line results, reusing the existing bounded grep implementation. HTML requests without the s parameter show the search form without running a search. JSON behavior is unchanged and still requires s. Issue: 376381593 Change-Id: Ie5b2a513d61fb4c44d6efba868ae6faa9aa866d0 Signed-off-by: Devansh Varshney <devansh@libreoffice.org>
diff --git a/java/com/google/gitiles/GitilesFilter.java b/java/com/google/gitiles/GitilesFilter.java index 3f35e9f..b696841 100644 --- a/java/com/google/gitiles/GitilesFilter.java +++ b/java/com/google/gitiles/GitilesFilter.java
@@ -264,7 +264,7 @@ case LOG: return new LogServlet(accessFactory, renderer, linkifier()); case GREP: - return new GrepServlet(accessFactory); + return new GrepServlet(accessFactory, renderer); case DESCRIBE: return new DescribeServlet(accessFactory); case ARCHIVE:
diff --git a/java/com/google/gitiles/GrepServlet.java b/java/com/google/gitiles/GrepServlet.java index bae8139..8f3e698 100644 --- a/java/com/google/gitiles/GrepServlet.java +++ b/java/com/google/gitiles/GrepServlet.java
@@ -18,10 +18,12 @@ import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.google.gitiles.GitilesRequestFailureException.FailureReason; import com.google.gson.reflect.TypeToken; import java.io.IOException; import java.util.List; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jgit.diff.RawText; @@ -48,8 +50,30 @@ @VisibleForTesting static final int MAX_MATCHES = 1000; private static final int MAX_BLOB_SIZE = 1 << 20; // 1 MB - protected GrepServlet(GitilesAccess.Factory accessFactory) { - super(null, accessFactory); + protected GrepServlet(GitilesAccess.Factory accessFactory, Renderer renderer) { + super(renderer, accessFactory); + } + + @Override + protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException { + GitilesView view = ViewFilter.getView(req); + String substring = + Strings.nullToEmpty(Iterables.getFirst(view.getParameters().get(SUBSTRING_PARAM), null)); + + List<Map<String, Object>> matches = Lists.newArrayList(); + if (!substring.isEmpty()) { + matches = toSoyMatches(view, grep(ServletUtils.getRepository(req), view, substring).matches); + } + + Map<String, Object> data = Maps.newHashMapWithExpectedSize(6); + data.put("title", "Grep - " + view.getRevision().getName()); + data.put("revision", view.getRevision().getName()); + data.put("path", Strings.nullToEmpty(view.getPathPart())); + data.put("substring", substring); + data.put("hasSearch", !substring.isEmpty()); + data.put("matches", matches); + + renderHtml(req, res, "com.google.gitiles.templates.GrepDetail.grepDetail", data); } @Override @@ -82,6 +106,14 @@ return new GrepResult(matches); } + private static List<Map<String, Object>> toSoyMatches(GitilesView view, List<Match> matches) { + List<Map<String, Object>> data = Lists.newArrayListWithCapacity(matches.size()); + for (Match match : matches) { + data.add(match.toSoyData(view)); + } + return data; + } + private static void grepPath( ObjectReader reader, RevTree root, String path, String substring, List<Match> matches) throws IOException { @@ -204,5 +236,15 @@ this.lineNumber = lineNumber; this.line = line; } + + Map<String, Object> toSoyData(GitilesView view) { + Map<String, Object> data = Maps.newHashMapWithExpectedSize(4); + data.put("path", path); + data.put("lineNumber", lineNumber); + data.put("line", line); + data.put( + "url", GitilesView.path().copyFrom(view).setPathPart(path).toUrl() + "#" + lineNumber); + return data; + } } }
diff --git a/java/com/google/gitiles/Renderer.java b/java/com/google/gitiles/Renderer.java index 0087e04..fe22701 100644 --- a/java/com/google/gitiles/Renderer.java +++ b/java/com/google/gitiles/Renderer.java
@@ -81,6 +81,7 @@ "DiffDetail.soy", "Doc.soy", "Error.soy", + "GrepDetail.soy", "HostIndex.soy", "LogDetail.soy", "ObjectDetail.soy",
diff --git a/javatests/com/google/gitiles/GrepServletTest.java b/javatests/com/google/gitiles/GrepServletTest.java index 2ccd951..b940f44 100644 --- a/javatests/com/google/gitiles/GrepServletTest.java +++ b/javatests/com/google/gitiles/GrepServletTest.java
@@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_OK; import java.util.List; import org.junit.Test; @@ -55,6 +56,33 @@ } @Test + public void grepHtmlShowsSearchForm() throws Exception { + repo.branch("master").commit().add("foo", "contents").create(); + + FakeHttpServletResponse res = buildResponse("/repo/+grep/master", "format=html", SC_OK); + + assertThat(res.getActualBodyString()).contains("name=\"s\""); + assertThat(res.getActualBodyString()).contains("Search"); + } + + @Test + public void grepHtmlSearchesFileContents() throws Exception { + repo.branch("master") + .commit() + .add("dir/a.txt", "alpha\nneedle here\n") + .add("dir/b.txt", "no match\n") + .create(); + + FakeHttpServletResponse res = + buildResponse("/repo/+grep/master", "format=html&s=needle", SC_OK); + + assertThat(res.getActualBodyString()).contains("dir/a.txt"); + assertThat(res.getActualBodyString()).contains("2: needle here"); + assertThat(res.getActualBodyString()).contains("#2"); + assertThat(res.getActualBodyString()).doesNotContain("dir/b.txt"); + } + + @Test public void grepJsonDoesNotSearchNonExistingPathPrefix() throws Exception { repo.branch("master").commit().add("src/a.txt", "needle\n").create();
diff --git a/resources/com/google/gitiles/static/base.css b/resources/com/google/gitiles/static/base.css index 095cc99..d5054e7 100644 --- a/resources/com/google/gitiles/static/base.css +++ b/resources/com/google/gitiles/static/base.css
@@ -502,6 +502,12 @@ } } +/* GrepDetail.soy */ + +.GrepResults-item { + margin-bottom: 15px; +} + /* LogDetail.soy */ .LogNav {
diff --git a/resources/com/google/gitiles/templates/GrepDetail.soy b/resources/com/google/gitiles/templates/GrepDetail.soy new file mode 100644 index 0000000..5dedc25 --- /dev/null +++ b/resources/com/google/gitiles/templates/GrepDetail.soy
@@ -0,0 +1,71 @@ +// Copyright 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. +{namespace com.google.gitiles.templates.GrepDetail} + +import * as common from 'com/google/gitiles/templates/Common.soy'; + +/** + * Detail page for file-content search results. + */ +{template grepDetail stricthtml="false"} + {@param title: ?} + {@param repositoryName: ?} + {@param? menuEntries: ?} + {@param? customVariant: ?} + {@param breadcrumbs: ?} + {@param revision: string} + {@param path: string} + {@param substring: string} + {@param hasSearch: bool} + {@param matches: list<?>} + +{call common.header data="all" /} + +<form method="GET" class="GrepSearch"> + <input type="text" name="s" value="{$substring}" class="GrepSearch-input"> + <button type="submit" class="GrepSearch-button"> + {msg desc="button text for grep search"}Search{/msg} + </button> +</form> + +{if $path} + <p class="GrepSearch-path"> + {msg desc="search path label"}Searching in{/msg} + {sp}<span class="u-monospace">{$path}</span> + </p> +{/if} + +{if $hasSearch} + {if length($matches)} + <ol class="GrepResults"> + {for $match in $matches} + <li class="GrepResults-item"> + <a class="u-monospace" href="{$match.url}"> + {$match.path} + </a> + <pre class="u-pre GrepResults-line"><code class="u-monospace">{$match.lineNumber}: {$match.line}</code></pre> + </li> + {/for} + </ol> + {else} + <p class="GrepResults-empty"> + {msg desc="message shown when grep search has no matches"}No matches.{/msg} + </p> + {/if} +{/if} + +{call common.footer} + {param customVariant: $customVariant /} +{/call} +{/template}