Merge changes I238c263b,Ifaa6867b,Id42dcb9e,Ie31b1db1,I3fca94f2, ...

* changes:
  Update JGit to b1cc74b75
  Update jetty to 9.4.53.v20231009
  Update byte-buddy to 1.14.9
  Update org.mockito:mockito-core to 5.6.0
  Update com.ibm.icu:icu4j to 74.2
  Update ch.qos.reload4j:reload4j to 1.2.25
  Update com.google.code.gson:gson to 2.10.1
  Update commons-codec:commons-codec to 1.16.0
  Update com.googlecode.javaewah:JavaEWAH to 1.2.3
  Update org.apache.commons:commons-compress to 1.25.0
diff --git a/.bazelrc b/.bazelrc
index ac89666..7c7def3 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,3 +1,7 @@
+# TODO(davido): Migrate all dependencies from WORKSPACE to MODULE.bazel
+# https://issues.gerritcodereview.com/issues/303819949
+common --noenable_bzlmod
+
 build --workspace_status_command="python3 ./tools/workspace_status.py"
 build --repository_cache=~/.gerritcodereview/bazel-cache/repository
 build --experimental_strict_action_env
diff --git a/.bazelversion b/.bazelversion
index 91e4a9f..9fe9ff9 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-6.3.2
+7.0.1
diff --git a/WORKSPACE b/WORKSPACE
index bdf86a3..2ff406b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -12,7 +12,7 @@
 load("//tools:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "e68cc7a45d9ee2b100024b9b12533b50a4598585",
+    commit = "50f43f450f2178425b26d5b2a2442ac3acd07f37",
     # local_path = "/home/<user>/projects/bazlets",
 )
 
@@ -140,8 +140,8 @@
 
 maven_jar(
     name = "soy",
-    artifact = "com.google.template:soy:2023-12-13",
-    sha1 = "8b63495fba832cd93c8474a11812668876fee05c",
+    artifact = "com.google.template:soy:2024-01-30",
+    sha1 = "6e9ccb00926325c7a9293ed05a2eaf56ea15d60e",
 )
 
 FLOGGER_VERS = "0.7.4"
diff --git a/java/com/google/gitiles/BlobSoyData.java b/java/com/google/gitiles/BlobSoyData.java
index a6879ae..c90b434 100644
--- a/java/com/google/gitiles/BlobSoyData.java
+++ b/java/com/google/gitiles/BlobSoyData.java
@@ -26,6 +26,7 @@
 import com.google.template.soy.data.SoyListData;
 import com.google.template.soy.data.SoyMapData;
 import java.io.IOException;
+import java.net.URI;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -79,6 +80,11 @@
 
   public Map<String, Object> toSoyData(String path, ObjectId blobId)
       throws MissingObjectException, IOException {
+    return toSoyData(path, blobId, null);
+  }
+
+  public Map<String, Object> toSoyData(String path, ObjectId blobId, @Nullable URI editUrl)
+      throws MissingObjectException, IOException {
     Map<String, Object> data = Maps.newHashMapWithExpectedSize(4);
     data.put("sha", ObjectId.toString(blobId));
 
@@ -119,6 +125,9 @@
       data.put("fileUrl", GitilesView.path().copyFrom(view).toUrl());
       data.put("logUrl", GitilesView.log().copyFrom(view).toUrl());
       data.put("blameUrl", GitilesView.blame().copyFrom(view).toUrl());
+      if (editUrl != null) {
+        data.put("editUrl", editUrl.toString());
+      }
       if (imageBlob != null) {
         data.put("imgBlob", imageBlob);
       }
diff --git a/java/com/google/gitiles/GitilesView.java b/java/com/google/gitiles/GitilesView.java
index 9bb43a9..f5be973 100644
--- a/java/com/google/gitiles/GitilesView.java
+++ b/java/com/google/gitiles/GitilesView.java
@@ -32,6 +32,7 @@
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Multimaps;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.util.ArrayList;
@@ -299,6 +300,12 @@
       return this;
     }
 
+    @CanIgnoreReturnValue
+    public Builder removeParam(String key) {
+      params.removeAll(key);
+      return this;
+    }
+
     public Builder putAllParams(Map<String, String[]> params) {
       for (Map.Entry<String, String[]> e : params.entrySet()) {
         this.params.putAll(e.getKey(), Arrays.asList(e.getValue()));
diff --git a/java/com/google/gitiles/LogSoyData.java b/java/com/google/gitiles/LogSoyData.java
index 061ce23..4292bc9 100644
--- a/java/com/google/gitiles/LogSoyData.java
+++ b/java/com/google/gitiles/LogSoyData.java
@@ -71,7 +71,10 @@
     this.view = checkNotNull(ViewFilter.getView(req));
     checkNotNull(pretty);
     Config config = access.getConfig();
-    fields = config.getBoolean("logFormat", pretty, "verbose", false) ? VERBOSE_FIELDS : FIELDS;
+    fields =
+        config.getBoolean("logFormat", pretty, "verbose", false) || pretty.equals("fuller")
+            ? VERBOSE_FIELDS
+            : FIELDS;
     variant = firstNonNull(config.getString("logFormat", pretty, "variant"), pretty);
   }
 
@@ -120,7 +123,9 @@
     ObjectId prev = paginator.getPreviousStart();
     if (prev != null) {
       GitilesView.Builder prevView = copyAndCanonicalizeView(revision);
-      if (!prevView.getRevision().getId().equals(prev)) {
+      if (prevView.getRevision().getId().equals(prev)) {
+        prevView.removeParam(LogServlet.START_PARAM);
+      } else {
         prevView.replaceParam(LogServlet.START_PARAM, prev.name());
       }
       data.put("previousUrl", prevView.toUrl());
diff --git a/java/com/google/gitiles/PathServlet.java b/java/com/google/gitiles/PathServlet.java
index 87d2680..fc0af05 100644
--- a/java/com/google/gitiles/PathServlet.java
+++ b/java/com/google/gitiles/PathServlet.java
@@ -15,6 +15,7 @@
 package com.google.gitiles;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gitiles.GitilesUrls.escapeName;
 import static com.google.gitiles.TreeSoyData.resolveTargetUrl;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
@@ -31,6 +32,8 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.Writer;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -49,6 +52,8 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -61,11 +66,14 @@
 import org.eclipse.jgit.util.QuotedString;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** Serves an HTML page with detailed information about a path within a tree. */
 // TODO(dborowitz): Handle non-UTF-8 names.
 public class PathServlet extends BaseServlet {
   private static final long serialVersionUID = 1L;
+  private static final Logger log = LoggerFactory.getLogger(PathServlet.class);
 
   static final String MODE_HEADER = "X-Gitiles-Path-Mode";
   static final String TYPE_HEADER = "X-Gitiles-Object-Type";
@@ -532,10 +540,34 @@
     return p.eof() ? p : null;
   }
 
+  private @Nullable URI createEditUrl(HttpServletRequest req, GitilesView view)
+      throws IOException {
+      String baseGerritUrl = this.urls.getBaseGerritUrl(req);
+      if (baseGerritUrl == null) {
+        return null;
+      }
+      String commitish = view.getRevision().getName();
+      if (commitish == null || !commitish.startsWith("refs/heads/")) {
+        return null;
+      }
+      try {
+        URI editUrl = new URI(baseGerritUrl);
+        String path =
+          String.format("/admin/repos/edit/repo/%s/branch/%s/file/%s",
+            view.getRepositoryName(),
+            commitish,
+            view.getPathPart());
+        return editUrl.resolve(escapeName(path));
+      } catch (URISyntaxException use) {
+        log.warn("Malformed Edit URL", use);
+      }
+      return null;
+  }
+
   private void showFile(HttpServletRequest req, HttpServletResponse res, WalkResult wr)
       throws IOException {
     GitilesView view = ViewFilter.getView(req);
-    Map<String, ?> data = new BlobSoyData(wr.getObjectReader(), view).toSoyData(wr.path, wr.id);
+    Map<String, ?> data = new BlobSoyData(wr.getObjectReader(), view).toSoyData(wr.path, wr.id, createEditUrl(req, view));
     // TODO(sop): Allow caching files by SHA-1 when no S cookie is sent.
     renderHtml(
         req,
diff --git a/java/com/google/gitiles/VisibilityChecker.java b/java/com/google/gitiles/VisibilityChecker.java
index fd81396..dedf547 100644
--- a/java/com/google/gitiles/VisibilityChecker.java
+++ b/java/com/google/gitiles/VisibilityChecker.java
@@ -68,8 +68,8 @@
       return false;
     }
 
-    return !walk.createReachabilityChecker()
-        .areAllReachable(ImmutableList.of(commit), startCommits)
+    return !walk.getObjectReader().createReachabilityChecker(walk)
+        .areAllReachable(ImmutableList.of(commit), startCommits.stream())
         .isPresent();
   }
 
diff --git a/java/com/google/gitiles/blame/cache/BlameCacheImpl.java b/java/com/google/gitiles/blame/cache/BlameCacheImpl.java
index 2fa80f5..2d831a4 100644
--- a/java/com/google/gitiles/blame/cache/BlameCacheImpl.java
+++ b/java/com/google/gitiles/blame/cache/BlameCacheImpl.java
@@ -163,7 +163,7 @@
     }
   }
 
-  private static List<Region> loadRegions(BlameGenerator gen) throws IOException {
+  public static List<Region> loadRegions(BlameGenerator gen) throws IOException {
     Map<ObjectId, PooledCommit> commits = Maps.newHashMap();
     Interner<String> strings = Interners.newStrongInterner();
     int lineCount = gen.getResultContents().size();
diff --git a/java/com/google/gitiles/blame/cache/Region.java b/java/com/google/gitiles/blame/cache/Region.java
index 17d280f..cd99043 100644
--- a/java/com/google/gitiles/blame/cache/Region.java
+++ b/java/com/google/gitiles/blame/cache/Region.java
@@ -53,7 +53,7 @@
     return start;
   }
 
-  int getEnd() {
+  public int getEnd() {
     return start + count;
   }
 
diff --git a/java/com/google/gitiles/dev/DevServer.java b/java/com/google/gitiles/dev/DevServer.java
index 028edd6..675511b 100644
--- a/java/com/google/gitiles/dev/DevServer.java
+++ b/java/com/google/gitiles/dev/DevServer.java
@@ -212,7 +212,7 @@
 
         @Override
         public Object getUserKey() {
-          return null;
+          return "";
         }
 
         @Override
diff --git a/javatests/com/google/gitiles/LogServletTest.java b/javatests/com/google/gitiles/LogServletTest.java
index 4ef7a4d..d875e0e 100644
--- a/javatests/com/google/gitiles/LogServletTest.java
+++ b/javatests/com/google/gitiles/LogServletTest.java
@@ -15,6 +15,7 @@
 package com.google.gitiles;
 
 import static com.google.common.truth.Truth.assertThat;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
 
 import com.google.gitiles.CommitJsonData.Commit;
 import com.google.gitiles.CommitJsonData.Log;
@@ -30,6 +31,10 @@
 @RunWith(JUnit4.class)
 public class LogServletTest extends ServletTest {
   private static final TypeToken<Log> LOG = new TypeToken<Log>() {};
+  private static final String MAIN = "main";
+  private static final String AUTHOR_METADATA_ELEMENT = "<th class=\"Metadata-title\">author</th>";
+  private static final String COMMITTER_METADATA_ELEMENT =
+      "<th class=\"Metadata-title\">committer</th>";
 
   @Test
   public void basicLog() throws Exception {
@@ -136,4 +141,125 @@
     assertThat(jsonCommit.committer.time).isEqualTo(df.format(commit.getCommitterIdent()));
     assertThat(jsonCommit.message).isEqualTo(commit.getFullMessage());
   }
+
+  @Test
+  public void verifyPreviousButtonAction() throws Exception {
+    repo.branch(MAIN).commit().add("foo", "contents").create();
+    RevCommit grandParent = repo.branch(MAIN).commit().add("foo", "contents").create();
+    RevCommit parent =
+        repo.branch(MAIN).commit().parent(grandParent).add("foo", "contents").create();
+    RevCommit main = repo.branch(MAIN).commit().parent(parent).create();
+
+    int numCommitsPerPage = 2;
+    String path =
+        "/repo/+log/" + grandParent.toObjectId().getName() + ".." + main.toObjectId().getName();
+    FakeHttpServletResponse res =
+        buildResponse(
+            path,
+            "format=html" + "&n=" + numCommitsPerPage + "&s=" + parent.toObjectId().getName(),
+            SC_OK);
+
+    assertThat(res.getActualBodyString())
+        .contains(
+            "<a class=\"LogNav-prev\""
+                + " href=\"/b/repo/+log/"
+                + grandParent.toObjectId().getName()
+                + ".."
+                + main.toObjectId().getName()
+                + "/?format=html"
+                + "&amp;n=2"
+                + "\">");
+  }
+
+  @Test
+  public void verifyNextButtonAction() throws Exception {
+    repo.branch(MAIN).commit().add("foo", "contents").create();
+    RevCommit grandParent = repo.branch(MAIN).commit().add("foo", "contents").create();
+    RevCommit parent =
+        repo.branch(MAIN).commit().parent(grandParent).add("foo", "contents").create();
+    RevCommit main = repo.branch(MAIN).commit().parent(parent).create();
+
+    int numCommitsPerPage = 1;
+    String path =
+        "/repo/+log/" + grandParent.toObjectId().getName() + ".." + main.toObjectId().getName();
+    FakeHttpServletResponse res =
+        buildResponse(path, "format=html" + "&n=" + numCommitsPerPage, SC_OK);
+
+    assertThat(res.getActualBodyString())
+        .contains(
+            "<a class=\"LogNav-next\""
+                + " href=\"/b/repo/+log/"
+                + grandParent.toObjectId().getName()
+                + ".."
+                + main.toObjectId().getName()
+                + "/?format=html"
+                + "&amp;n=1"
+                + "&amp;s="
+                + parent.toObjectId().getName()
+                + "\">");
+  }
+
+  @Test
+  public void prettyDefaultUsesDefaultCssClass() throws Exception {
+    RevCommit parent = repo.branch(MAIN).commit().add("foo", "contents").create();
+    RevCommit main = repo.branch(MAIN).commit().parent(parent).create();
+
+    String path =
+        "/repo/+log/" + parent.toObjectId().getName() + ".." + main.toObjectId().getName();
+    FakeHttpServletResponse res = buildResponse(path, "format=html", SC_OK);
+
+    assertThat(res.getActualBodyString())
+        .contains("<li class=\"CommitLog-item CommitLog-item--default\">");
+    assertThat(res.getActualBodyString()).doesNotContain(AUTHOR_METADATA_ELEMENT);
+    assertThat(res.getActualBodyString()).doesNotContain(COMMITTER_METADATA_ELEMENT);
+  }
+
+  @Test
+  public void prettyExplicitlyDefaultUsesDefaultCssClass() throws Exception {
+    testPrettyHtmlOutput(
+        "default", /* shouldShowAuthor= */ false, /* shouldShowCommitter= */ false);
+  }
+
+  @Test
+  public void prettyOnelineUsesOnelineCssClass() throws Exception {
+    testPrettyHtmlOutput(
+        "oneline", /* shouldShowAuthor= */ false, /* shouldShowCommitter= */ false);
+  }
+
+  @Test
+  public void prettyCustomTypeUsesCustomCssClass() throws Exception {
+    testPrettyHtmlOutput(
+        "aCustomPrettyType", /* shouldShowAuthor= */ false, /* shouldShowCommitter= */ false);
+  }
+
+  @Test
+  public void prettyFullerUsesFullerCssClass() throws Exception {
+    testPrettyHtmlOutput("fuller", /* shouldShowAuthor= */ true, /* shouldShowCommitter= */ true);
+  }
+
+  private void testPrettyHtmlOutput(
+      String prettyType, boolean shouldShowAuthor, boolean shouldShowCommitter) throws Exception {
+    RevCommit parent = repo.branch(MAIN).commit().add("foo", "contents").create();
+    RevCommit main = repo.branch(MAIN).commit().parent(parent).create();
+
+    String path =
+        "/repo/+log/" + parent.toObjectId().getName() + ".." + main.toObjectId().getName();
+    FakeHttpServletResponse res =
+        buildResponse(path, "format=html" + "&pretty=" + prettyType, SC_OK);
+
+    assertThat(res.getActualBodyString())
+        .contains("<li class=\"CommitLog-item CommitLog-item--" + prettyType + "\">");
+
+    if (shouldShowAuthor) {
+      assertThat(res.getActualBodyString()).contains(AUTHOR_METADATA_ELEMENT);
+    } else {
+      assertThat(res.getActualBodyString()).doesNotContain(AUTHOR_METADATA_ELEMENT);
+    }
+
+    if (shouldShowCommitter) {
+      assertThat(res.getActualBodyString()).contains(COMMITTER_METADATA_ELEMENT);
+    } else {
+      assertThat(res.getActualBodyString()).doesNotContain(COMMITTER_METADATA_ELEMENT);
+    }
+  }
 }
diff --git a/javatests/com/google/gitiles/PathServletTest.java b/javatests/com/google/gitiles/PathServletTest.java
index 5002948..7531242 100644
--- a/javatests/com/google/gitiles/PathServletTest.java
+++ b/javatests/com/google/gitiles/PathServletTest.java
@@ -103,6 +103,18 @@
   }
 
   @Test
+  public void editUrl() throws Exception {
+    repo.branch("master").commit().add("editFoo", "Content").create();
+    repo.reset("refs/heads/master");
+
+    Map<String, ?> data = buildData("/repo/+/refs/heads/master/editFoo");
+
+    String editUrl = (String) getBlobData(data).get("editUrl");
+    String testUrl = "http://test-host-review/admin/repos/edit/repo/repo/branch/refs/heads/master/file/editFoo";
+    assertThat(editUrl).isEqualTo(testUrl);
+  }
+
+  @Test
   public void fileWithMaxLines() throws Exception {
     int MAX_LINE_COUNT = 50000;
     StringBuilder contentBuilder = new StringBuilder();
diff --git a/javatests/com/google/gitiles/doc/DocServletTest.java b/javatests/com/google/gitiles/doc/DocServletTest.java
index 2a9c2c5..110f481 100644
--- a/javatests/com/google/gitiles/doc/DocServletTest.java
+++ b/javatests/com/google/gitiles/doc/DocServletTest.java
@@ -158,7 +158,6 @@
     repo.branch("master").commit().add("index.md", markdown).create();
 
     String html = buildHtml("/repo/+/master/index.md");
-    System.out.println(html);
     assertThat(html)
         .contains(
             "<ul><li><p>one</p><div class=\"aside\">remember this</div>"
diff --git a/resources/com/google/gitiles/templates/HostIndex.soy b/resources/com/google/gitiles/templates/HostIndex.soy
index 846904a..353fd4f 100644
--- a/resources/com/google/gitiles/templates/HostIndex.soy
+++ b/resources/com/google/gitiles/templates/HostIndex.soy
@@ -34,7 +34,7 @@
 {/call}
 
 {if length($repositories)}
-  {if not $breadcrumbs}
+  {if !$breadcrumbs}
     <h1>
       {msg desc="Git repositories available on the host"}
         Git repositories on {$hostName}
diff --git a/resources/com/google/gitiles/templates/LogDetail.soy b/resources/com/google/gitiles/templates/LogDetail.soy
index 108bbef..f231aa2 100644
--- a/resources/com/google/gitiles/templates/LogDetail.soy
+++ b/resources/com/google/gitiles/templates/LogDetail.soy
@@ -285,7 +285,7 @@
       rename or copy. */
 {call logEntry variant="'full'" data="all" /}
 
-{if $diffTree and length($diffTree)}
+{if $diffTree && length($diffTree)}
   <ul class="DiffTree">
     {for $entry in $diffTree}
       <li>
diff --git a/resources/com/google/gitiles/templates/ObjectDetail.soy b/resources/com/google/gitiles/templates/ObjectDetail.soy
index 1dd3a40..f7c0b0c 100644
--- a/resources/com/google/gitiles/templates/ObjectDetail.soy
+++ b/resources/com/google/gitiles/templates/ObjectDetail.soy
@@ -242,12 +242,14 @@
   {@param? logUrl: ?}  /** optional URL to a log for this file. */
   {@param? blameUrl: ?}  /** optional URL to a blame for this file. */
   {@param? docUrl: ?}  /** optional URL to view rendered file. */
+  {@param? editUrl: ?} /** optional URL to create a change in Gerrit. */
 <div class="u-sha1 u-monospace BlobSha1">
   {msg desc="SHA-1 for the file's blob"}blob: {$sha}{/msg}
   {if $fileUrl}{sp}[<a href="{$fileUrl}">{msg desc="detail view of a file"}file{/msg}</a>]{/if}
   {if $logUrl}{sp}[<a href="{$logUrl}">{msg desc="history for a file"}log{/msg}</a>]{/if}
   {if $blameUrl}{sp}[<a href="{$blameUrl}">{msg desc="blame for a file"}blame{/msg}</a>]{/if}
   {if $docUrl}{sp}[<a href="{$docUrl}">{msg desc="view rendered file"}view{/msg}</a>]{/if}
+  {if $editUrl}{sp}[<a href="{$editUrl}">{msg desc="edit file in Gerrit"}edit{/msg}</a>]{/if}
 </div>
 {/template}
 
diff --git a/tools/run_dev.sh b/tools/run_dev.sh
index 1f4ed5e..b045164 100755
--- a/tools/run_dev.sh
+++ b/tools/run_dev.sh
@@ -35,7 +35,7 @@
 
 (
   cd "$ROOT"
-  bazel build java/com/google/gitiles/dev
+  "${BAZEL:-bazel}" build java/com/google/gitiles/dev
 )
 
 set -x
diff --git a/version.bzl b/version.bzl
index fef692b..eb86312 100644
--- a/version.bzl
+++ b/version.bzl
@@ -3,4 +3,4 @@
 # Used by :install and :deploy when talking to the destination repository.
 # Project uses semantic versioning described at:
 # https://semver.org
-GITILES_VERSION = "1.3.0"
+GITILES_VERSION = "1.4.0"