Merge "Respect .gitattributes diff overrides in diff operations"
diff --git a/.agents/rules.json b/.agents/rules.json
new file mode 100644
index 0000000..bcd4b27
--- /dev/null
+++ b/.agents/rules.json
@@ -0,0 +1,7 @@
+{
+ "entries": [
+ {
+ "path": ".agents/rules/upload_rationale.md"
+ }
+ ]
+}
diff --git a/.agents/rules/upload_rationale.md b/.agents/rules/upload_rationale.md
new file mode 100644
index 0000000..f7a13a4
--- /dev/null
+++ b/.agents/rules/upload_rationale.md
@@ -0,0 +1,18 @@
+---
+trigger: always_on
+description: Mandate uploading agent rationale for Gerrit changes
+---
+
+## Gerrit Rationale Upload Policy
+
+Whenever you create a new Gerrit change or upload a patchset (via `git push`,
+`hg upload`, `g4 upload`, or any other VCS command), you **MUST** immediately
+upload AI rationales for that patchset.
+
+Follow this two-step workflow:
+
+1. **Push Code**: Execute your VCS push/upload command so the new patchset is
+ created on Gerrit.
+2. **Upload Rationale**: Read the `gerrit` skill (specifically the
+ `upload-rationale` section) and follow its instructions to construct and
+ upload the complete set of rationales for the patchset.
diff --git a/.agents/skills.json b/.agents/skills.json
new file mode 100644
index 0000000..88387c1
--- /dev/null
+++ b/.agents/skills.json
@@ -0,0 +1,7 @@
+{
+ "entries": [
+ {
+ "path": "/google/src/files/head/depot/google3/learning/gemini/agents/skills/gerrit"
+ }
+ ]
+}
diff --git a/.aspect/rules/external_repository_action_cache/npm_translate_lock_LTE4Nzc1MDcwNjU= b/.aspect/rules/external_repository_action_cache/npm_translate_lock_LTE4Nzc1MDcwNjU=
new file mode 100755
index 0000000..b1e9051
--- /dev/null
+++ b/.aspect/rules/external_repository_action_cache/npm_translate_lock_LTE4Nzc1MDcwNjU=
@@ -0,0 +1,7 @@
+# @generated
+# Input hashes for repository rule npm_translate_lock(name = "npm", pnpm_lock = "@@//:pnpm-lock.yaml").
+# This file should be checked into version control along with the pnpm-lock.yaml file.
+package.json=259674664
+pnpm-lock.yaml=640391753
+pnpm-workspace.yaml=-408937369
+yarn.lock=742513459
diff --git a/.aspect/rules/external_repository_action_cache/npm_translate_lock_NDEzOTg2ODE= b/.aspect/rules/external_repository_action_cache/npm_translate_lock_NDEzOTg2ODE=
new file mode 100755
index 0000000..51238c9
--- /dev/null
+++ b/.aspect/rules/external_repository_action_cache/npm_translate_lock_NDEzOTg2ODE=
@@ -0,0 +1,7 @@
+# @generated
+# Input hashes for repository rule npm_translate_lock(name = "tools_npm", pnpm_lock = "@@//tools/node_tools:pnpm-lock.yaml").
+# This file should be checked into version control along with the pnpm-lock.yaml file.
+tools/node_tools/package.json=899646160
+tools/node_tools/pnpm-lock.yaml=-1121876554
+tools/node_tools/pnpm-workspace.yaml=-408937369
+tools/node_tools/yarn.lock=1865795164
diff --git a/.aspect/rules/external_repository_action_cache/npm_translate_lock_ODE4NDE3MDEy b/.aspect/rules/external_repository_action_cache/npm_translate_lock_ODE4NDE3MDEy
new file mode 100755
index 0000000..3612935
--- /dev/null
+++ b/.aspect/rules/external_repository_action_cache/npm_translate_lock_ODE4NDE3MDEy
@@ -0,0 +1,7 @@
+# @generated
+# Input hashes for repository rule npm_translate_lock(name = "ui_npm", pnpm_lock = "@@//polygerrit-ui/app:pnpm-lock.yaml").
+# This file should be checked into version control along with the pnpm-lock.yaml file.
+polygerrit-ui/app/package.json=-844837576
+polygerrit-ui/app/pnpm-lock.yaml=1583889522
+polygerrit-ui/app/pnpm-workspace.yaml=1396467527
+polygerrit-ui/app/yarn.lock=-909422203
diff --git a/.aspect/rules/external_repository_action_cache/npm_translate_lock_ODE4OTg1MjQ0 b/.aspect/rules/external_repository_action_cache/npm_translate_lock_ODE4OTg1MjQ0
new file mode 100755
index 0000000..33edcc5
--- /dev/null
+++ b/.aspect/rules/external_repository_action_cache/npm_translate_lock_ODE4OTg1MjQ0
@@ -0,0 +1,7 @@
+# @generated
+# Input hashes for repository rule npm_translate_lock(name = "ui_dev_npm", pnpm_lock = "@@//polygerrit-ui:pnpm-lock.yaml").
+# This file should be checked into version control along with the pnpm-lock.yaml file.
+polygerrit-ui/package.json=-860495337
+polygerrit-ui/pnpm-lock.yaml=201412925
+polygerrit-ui/pnpm-workspace.yaml=-408937369
+polygerrit-ui/yarn.lock=1276229348
diff --git a/.aspect/rules/external_repository_action_cache/npm_translate_lock_ODUwOTM3NTg= b/.aspect/rules/external_repository_action_cache/npm_translate_lock_ODUwOTM3NTg=
new file mode 100755
index 0000000..23457d5
--- /dev/null
+++ b/.aspect/rules/external_repository_action_cache/npm_translate_lock_ODUwOTM3NTg=
@@ -0,0 +1,7 @@
+# @generated
+# Input hashes for repository rule npm_translate_lock(name = "plugins_npm", pnpm_lock = "@@//plugins:pnpm-lock.yaml").
+# This file should be checked into version control along with the pnpm-lock.yaml file.
+plugins/package.json=1633560207
+plugins/pnpm-lock.yaml=-77521523
+plugins/pnpm-workspace.yaml=-408937369
+plugins/yarn.lock=-2081175573
diff --git a/.bazelrc b/.bazelrc
index b864a3b..f5d330f 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -4,6 +4,7 @@
common --incompatible_disallow_struct_provider_syntax=false
common --incompatible_disallow_empty_glob=false
common --@aspect_rules_ts//ts:default_to_tsc_transpiler
+common --repo_env=ASPECT_TOOLS_TELEMETRY=-all
build --workspace_status_command="python3 ./tools/workspace_status.py"
build --repository_cache=~/.gerritcodereview/bazel-cache/repository
diff --git a/.gitignore b/.gitignore
index 2d146c8..f42cfc0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,9 +10,9 @@
.DS_Store
CLAUDE.md
js-to-ts.sh
-/.aspect/
/.apt_generated
/.apt_generated_tests
+/.eslintcache
/.bazel_path
/.classpath
/.factorypath
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..ca5c350
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+24.18.0
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index 65f05b1..ceca34b 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -8,20 +8,39 @@
--
_ssh_ -p <port> <host> _gerrit show-caches_
[--show-jvm]
+ [--include-diskstats]
+ [--cache <NAME> ...]
--
== DESCRIPTION
Display statistics about the size and hit ratio of in-memory caches.
+By default, the command displays statistics for all registered caches.
+Because collecting data for numerous large caches on a busy server
+can cause delays, the --cache option can be used (one or more times)
+to limit the output to specific caches.
+
== OPTIONS
--show-jvm::
List the name and version of the Java virtual machine, host
operating system, and other details about the environment
that Gerrit Code Review is running in.
+--include-diskstats::
+ Include disk stat collection for persistent caches.
+
--show-threads::
Show detailed counts for Gerrit specific threads.
+--cache <NAME>::
+ Show statistics only for the cache called <NAME>. May be
+ supplied more than once to display multiple caches in a single
+ command execution. Cache names are matched case-insensitively
+ against the full registered cache name (including any plugin
+ prefix). Unknown cache names are silently ignored. The
+ summary sections (SSH, tasks, memory, threads, JVM) are not
+ affected by this option.
+
--width::
-w::
Width of the output table.
@@ -84,6 +103,12 @@
Threads: 4 CPUs available, 371 threads
----
+Show statistics for only the "accounts" and "projects" caches:
+
+----
+$ ssh -p 29418 review.example.com gerrit show-caches --cache accounts --cache projects
+----
+
== SEE ALSO
* link:cmd-flush-caches.html[gerrit flush-caches]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 42b4c10d..d8c1f6b 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -430,6 +430,26 @@
HTTP header to trust the username from, or unset to select HTTP basic
authentication. Only used if `auth.type` is set to `HTTP`.
+[[auth.httpTrustedProxyNetworks]]auth.httpTrustedProxyNetworks::
++
+List of IPv4 networks CIDRs (e.g., `192.168.0.0/16`) that are trusted proxies for receiving
+HTTP header authentication. Specify multiple networks by adding multiple entries.
+It applies only when `auth.type` is set to `HTTP` or `HTTP_LDAP`. The trusted-proxy
+remote address must be IPv4; if Gerrit is reached over IPv6, header-based
+authentication is rejected.
++
+[NOTE]
+====
+Accepting a trusted authentication HTTP header involves a leap of faith in the security
+of the incoming HTTP traffic. By default, the only secure way to ensure that all the
+traffic is coming from a trusted authentication proxy is via firewall rules or using
+a local proxy on the loopback interface. When using a remote proxy, the security of
+the connection between the proxy and Gerrit is in doubt, and this can be mitigated by
+the use of `auth.httpTrustedProxyNetworks`.
+====
++
+Default: accept any incoming IPs.
+
[[auth.httpDisplaynameHeader]]auth.httpDisplaynameHeader::
+
HTTP header to retrieve the user's display name from. Only used if `auth.type`
@@ -912,6 +932,32 @@
Valid values are 0, and positive integers. Setting this to 0 will
cause the filter to never be rebuilt.
+
+[[cache.startupThreads]]cache.startupThreads::
++
+Number of threads used to open persistent caches in parallel during startup.
+Each persistent cache is backed by its own H2 database file and builds a
+BloomFilter by reading all keys from disk when it is opened. With the default
+of 1, caches are opened sequentially. Increasing this value allows the
+BloomFilter build phase to run concurrently across caches.
++
+Setting this value greater than 1 creates threads in a dedicated pool, separate
+from the H2 thread pool (`DiskCache-Store`) that handles cache updates.
++
+Default is 1.
+
+[[cache.preWarmForBloomFilter]]cache.preWarmForBloomFilter::
++
+When enabled, each persistent cache reads its H2 database file into the OS
+page cache before building its BloomFilter. The BloomFilter build requires
+a full table scan of all keys which involves reading close to the entire file
+in a scattered fashion. Doing such a read tends to not perform well I/O wise as
+it generally cannot take advantage of operating system level file system
+readahead. Reading the file sequentially up front tends to take advantage of
+readahead to fully populate the file system caches which then can make scattered
+reads much faster.
++
+Default is true.
+
[[cache.openFiles]]cache.openFiles::
+
The number of file descriptors to add to the limit set by the Gerrit daemon.
@@ -5080,6 +5126,7 @@
[[plugins.loadPriority]]plugins.loadPriority::
+
List of `pluginName`s required to have a specific loading order during Gerrit startup.
+Plugins will be unloaded in reverse order.
+
Each entry should contain a plugin name defined in the `MANIFEST.MF` under
`Gerrit-PluginName` or a plugin JAR file name. During the Gerrit startup
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 49ec3f4..c440eb3 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -226,7 +226,7 @@
$change.shortOriginalSubject::
+
-The original subject limited to 72 characters, with an ellipsis if it exceeds
+The original subject limited to 72 characters, with an ellipsis if it exceeds.
that.
$change.sizeBucket::
@@ -251,7 +251,7 @@
$instanceAndProjectName::
+
-The Gerrit instance name, followed by the short project name
+The Gerrit instance name, followed by the short project name.
$addInstanceNameInSubject::
+
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index d8bfb77..bf26fc2 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -43,6 +43,35 @@
Filters on a folder, they will be overwritten the next time you run
`tools/eclipse/project.py`.
+=== JGit servlet-facing sources
+
+Gerrit consumes JGit from source through `modules/jgit`, but JGit master uses
+`jakarta.servlet` in servlet-facing modules while Gerrit still runs on Jetty 12
+EE8 and `javax.servlet`.
+
+For those modules, Gerrit uses JGit-provided Bazel EE8 bridge targets:
+
+----
+ @jgit//org.eclipse.jgit.http.server.ee8:jgit-servlet-ee8
+ @jgit//org.eclipse.jgit.lfs.server.ee8:jgit-lfs-server-ee8
+----
+
+The generated artifacts keep the original JGit Java packages and source line
+numbers. They only rewrite servlet imports from `jakarta.servlet` to
+`javax.servlet`, so Gerrit Java code keeps its existing imports.
+
+The Eclipse project generator does not import
+`modules/jgit/org.eclipse.jgit.http.server/src` as a source folder. Instead, it
+attaches the generated EE8 source jar when the generated servlet jar is on the
+classpath. Put breakpoints in the generated source attachment when debugging
+runtime HTTP servlet classes.
+
+The same source attachment rule applies to
+`org.eclipse.jgit.lfs.server.ee8:jgit-lfs-server-ee8` for plugin consumers such
+as the Gerrit LFS plugin. Core Gerrit does not depend on that jar today, but the
+Eclipse generator is prepared to attach its generated source jar when a plugin
+classpath includes it.
+
=== Eclipse project on MacOS
By default, bazel uses `/private/var/tmp` as the
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index be4196c..a4b7776 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -86,6 +86,45 @@
advantage of this is that project files don't need to be excluded from version
control.
+=== JGit servlet-facing sources
+
+Gerrit consumes JGit from source through `modules/jgit`, but JGit master uses
+`jakarta.servlet` in servlet-facing modules while Gerrit still runs on Jetty 12
+EE8 and `javax.servlet`.
+
+For those modules, Gerrit uses JGit-provided Bazel EE8 bridge targets:
+
+----
+ @jgit//org.eclipse.jgit.http.server.ee8:jgit-servlet-ee8
+ @jgit//org.eclipse.jgit.lfs.server.ee8:jgit-lfs-server-ee8
+----
+
+The generated artifacts keep the original JGit Java packages and source line
+numbers. They only rewrite servlet imports from `jakarta.servlet` to
+`javax.servlet`, so Gerrit Java code keeps its existing imports.
+
+Use the current JetBrains Bazel plugin (the new, actively developed one). The
+legacy "Bazel for IntelliJ" plugin does not support Bazel 9 / Bzlmod, which
+Gerrit requires. The Bazel plugin imports `libjgit-servlet-ee8.jar` together
+with its generated source jar, so the servlet classes resolve to the EE8
+`javax.servlet` sources. Put breakpoints there when debugging runtime HTTP
+servlet classes; because line numbers are preserved, breakpoints bind and step
+correctly. The same applies to `jgit-lfs-server-ee8` for plugin consumers such
+as the Gerrit LFS plugin.
+
+The canonical, editable sources stay in SCM under
+`modules/jgit/org.eclipse.jgit.http.server` (and `org.eclipse.jgit.lfs.server`).
+To change the servlet code, edit the canonical `jakarta.servlet` sources,
+rebuild, and the change flows into the generated EE8 artifacts.
+
+NOTE: The IDE may flag these classes with "Library source does not match the
+bytecode" and report unresolved `javax.servlet` symbols. This is a known
+limitation of the Bazel IntelliJ integration with generated and duplicate
+sources: the project indexes both the canonical `jakarta.servlet` source and
+the EE8 `javax.servlet` jar, which share class names. It is a static-analysis
+artifact only and does not affect debugging, which uses the class actually
+loaded at runtime.
+
== Recommended settings
=== Code style
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 7da1c18..5c13aef 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -208,12 +208,20 @@
* `caches/memory_eviction_count`: Memory eviction count.
** `cache_name`:
The name of the cache.
+* `caches/memory_request_count`: Total number of lookups (hits + misses) against
+ the in-memory cache.
+** `cache_name`:
+ The name of the cache.
* `caches/disk_cached`: Disk entries used by persistent cache.
** `cache_name`:
The name of the cache.
* `caches/disk_hit_ratio`: Disk hit ratio for persistent cache.
** `cache_name`:
The name of the cache.
+* `caches/disk_request_count`: Total number of lookups (hits + misses) against
+ the persistent disk cache.
+** `cache_name`:
+ The name of the cache.
* `caches/refresh_count`: The number of refreshes per cache with an indicator if
a reload was necessary.
** `cache`:
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index 80d20c3..ed63caa 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -68,6 +68,20 @@
current revision displayed, an instance of
link:rest-api-changes.html#revision-info[RevisionInfo]
+=== change-view-commit-aside
+The `change-view-commit-aside` extension point is located on the change view
+page, next to the commit message above the related changes list.
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
+
=== change-metadata-item
The `change-metadata-item` extension point is located on the change view
left panel, below the `Submit Requirements` and `Links` sections by default.
diff --git a/Documentation/release_war_jars.txt b/Documentation/release_war_jars.txt
index 953e2dc..099aae1 100644
--- a/Documentation/release_war_jars.txt
+++ b/Documentation/release_war_jars.txt
@@ -35,7 +35,7 @@
gson
guava-33.5.0-jre
guava-retrying
-guice
+guice-6.0.0-classes
guice-assistedinject
guice-servlet
h2
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index b86d732..f7e477d 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -330,11 +330,25 @@
The entries in the map are sorted by cache name.
+.Query Options
+[options="header",cols="1,3"]
+|===
+|Field Name |Description
+|`include-diskstats` |If `true`, disk stat collection is included for persistent caches.
+|===
+
.Request
----
GET /config/server/caches/ HTTP/1.0
----
+To include disk stats for persistent caches:
+
+.Request
+----
+ GET /config/server/caches/?include-diskstats=true HTTP/1.0
+----
+
.Response
----
HTTP/1.1 200 OK
diff --git a/MODULE.bazel b/MODULE.bazel
index e2c4cd4..55fbaa8 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -7,16 +7,25 @@
# Language rules.
bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
-bazel_dep(name = "aspect_rules_js", version = "3.1.2")
+bazel_dep(name = "aspect_rules_js", version = "3.2.3")
bazel_dep(name = "aspect_rules_rollup", version = "2.0.1")
-bazel_dep(name = "aspect_rules_ts", version = "3.8.8")
+bazel_dep(name = "aspect_rules_ts", version = "3.9.2")
bazel_dep(name = "rules_android", version = "0.7.2")
bazel_dep(name = "rules_go", version = "0.60.0")
bazel_dep(name = "rules_java", version = "9.3.0")
-bazel_dep(name = "rules_nodejs", version = "6.7.4")
+bazel_dep(name = "rules_nodejs", version = "6.7.5")
bazel_dep(name = "rules_python", version = "2.0.2")
bazel_dep(name = "rules_shell", version = "0.8.0")
+# Pin rules_java to Gerrit's chosen version. Transitive bzlmod modules can
+# otherwise raise it through MVS, which changes the Java toolchain inputs
+# and can break custom plugin builds. When merging up, keep this override
+# but update the version to the target branch's chosen rules_java version.
+single_version_override(
+ module_name = "rules_java",
+ version = "9.3.0",
+)
+
# Libraries / toolchains.
bazel_dep(name = "protobuf", version = "35.0")
@@ -51,4 +60,3 @@
# Wiring for external dependencies contributed by in-tree plugins.
include("//plugins:external_plugin_deps.MODULE.bazel")
-
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index a497a62..7e2e9de 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -7,13 +7,11 @@
"https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
- "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16",
"https://bcr.bazel.build/modules/abseil-cpp/20250127.0/MODULE.bazel": "d1086e248cda6576862b4b3fe9ad76a214e08c189af5b42557a6e1888812c5d5",
"https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1",
"https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215",
- "https://bcr.bazel.build/modules/abseil-cpp/20250814.0/MODULE.bazel": "c43c16ca2c432566cdb78913964497259903ebe8fb7d9b57b38e9f1425b427b8",
"https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2",
"https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c",
"https://bcr.bazel.build/modules/abseil-py/2.1.0/MODULE.bazel": "5ebe5bf853769c65707e5c28f216798f7a4b1042015e6a36e6d03094d94bec8a",
@@ -35,14 +33,14 @@
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838",
"https://bcr.bazel.build/modules/aspect_rules_js/2.0.0/MODULE.bazel": "b45b507574aa60a92796e3e13c195cd5744b3b8aff516a9c0cb5ae6a048161c5",
- "https://bcr.bazel.build/modules/aspect_rules_js/3.1.2/MODULE.bazel": "e3685502155d3cc65f3bf98e714f7435de67d7f8f355d63478a80197310311fc",
- "https://bcr.bazel.build/modules/aspect_rules_js/3.1.2/source.json": "a32ab71831452b945f3f83a1b1feb9402007e600bce55ac76e15ef0c1e08b520",
+ "https://bcr.bazel.build/modules/aspect_rules_js/3.2.3/MODULE.bazel": "3a0363a5b8ec4931488dc327c577a78c1f5a725293ebffaa5d2613283701aa8a",
+ "https://bcr.bazel.build/modules/aspect_rules_js/3.2.3/source.json": "fa3187962f5fdeee0c6226792904aaafb2cf585717a264fa4393a767bffb99fe",
"https://bcr.bazel.build/modules/aspect_rules_rollup/2.0.1/MODULE.bazel": "296e3a053658c2af989ba9bd62a205e6d1fa84bdd6dd5249196546e6b84770ec",
"https://bcr.bazel.build/modules/aspect_rules_rollup/2.0.1/source.json": "2fe8ac1ccb4de74bf884761e070010280b272d94e3997205b361b91c75409726",
- "https://bcr.bazel.build/modules/aspect_rules_ts/3.8.8/MODULE.bazel": "b52b929a948438665809d49af610f58d1b14f63d6d21ab748f47b6050be4c1f6",
- "https://bcr.bazel.build/modules/aspect_rules_ts/3.8.8/source.json": "5414530b761a45ab7ca6c49f0a2a9cf8dc0da772f5037cf05ca18aaa64bb1b19",
- "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/MODULE.bazel": "37c764292861c2f70314efa9846bb6dbb44fc0308903b3285da6528305450183",
- "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/source.json": "605086bbc197743a0d360f7ddc550a1d4dfa0441bc807236e17170f636153348",
+ "https://bcr.bazel.build/modules/aspect_rules_ts/3.9.2/MODULE.bazel": "feeb6c45b69c995eca3e5ca5872658c80df658022e01044eca00cf472bb89142",
+ "https://bcr.bazel.build/modules/aspect_rules_ts/3.9.2/source.json": "cf3075502f798f71a9c5707a7684eaf0b86da11ed440fd90ea34385dc297676a",
+ "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.4.2/MODULE.bazel": "f31aa84151d31e98cffd43eb7217ccff5ec52bdd5f2d10db8f053aeb23342eca",
+ "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.4.2/source.json": "d027d264e6b6e7fc421e38189f4374fcd14a67e0bd6e0e705de8c2185c3787e1",
"https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b",
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
"https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101",
@@ -180,7 +178,6 @@
"https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
"https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68",
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
- "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
"https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
"https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
"https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
@@ -212,28 +209,6 @@
"https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae",
"https://bcr.bazel.build/modules/rules_go/0.60.0/MODULE.bazel": "4a57ff2ffc2a3570e3c5646575c5a4b07287e91bcdac5d1f72383d51502b48cb",
"https://bcr.bazel.build/modules/rules_go/0.60.0/source.json": "1e21368c5e0c3013a110bd79a8fcff8ca46b5bcb2b561713a7273cbfcff7c464",
- "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
- "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
- "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39",
- "https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963",
- "https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6",
- "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
- "https://bcr.bazel.build/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64",
- "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
- "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
- "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
- "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2",
- "https://bcr.bazel.build/modules/rules_java/7.4.0/MODULE.bazel": "a592852f8a3dd539e82ee6542013bf2cadfc4c6946be8941e189d224500a8934",
- "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
- "https://bcr.bazel.build/modules/rules_java/8.13.0/MODULE.bazel": "0444ebf737d144cf2bb2ccb368e7f1cce735264285f2a3711785827c1686625e",
- "https://bcr.bazel.build/modules/rules_java/8.16.1/MODULE.bazel": "0f20b1cecaa8e52f60a8f071e59a20b4e3b9a67f6c56c802ea256f6face692d3",
- "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
- "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
- "https://bcr.bazel.build/modules/rules_java/8.6.0/MODULE.bazel": "9c064c434606d75a086f15ade5edb514308cccd1544c2b2a89bbac4310e41c71",
- "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2",
- "https://bcr.bazel.build/modules/rules_java/8.6.3/MODULE.bazel": "e90505b7a931d194245ffcfb6ff4ca8ef9d46b4e830d12e64817752e0198e2ed",
- "https://bcr.bazel.build/modules/rules_java/8.9.0/MODULE.bazel": "e17c876cb53dcd817b7b7f0d2985b710610169729e8c371b2221cacdcd3dce4a",
- "https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d",
"https://bcr.bazel.build/modules/rules_java/9.3.0/MODULE.bazel": "f657c72d65ac449caae9abf2e68e66c0d36f9416848c4c4903d0b3234229e7f2",
"https://bcr.bazel.build/modules/rules_java/9.3.0/source.json": "59ae7e662c3c7042b88bbb42ad12483523e234c65ebe4c51611baa43e85cb248",
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
@@ -262,8 +237,8 @@
"https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
"https://bcr.bazel.build/modules/rules_nodejs/6.2.0/MODULE.bazel": "ec27907f55eb34705adb4e8257952162a2d4c3ed0f0b3b4c3c1aad1fac7be35e",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056",
- "https://bcr.bazel.build/modules/rules_nodejs/6.7.4/MODULE.bazel": "e6a241a55c82e999145553d2e00a08fc6ebadf62b63d108fb5e984696ffd0bd2",
- "https://bcr.bazel.build/modules/rules_nodejs/6.7.4/source.json": "34e7a8a3b4c8d630ac0e0492b3fed9dba41fe008a0edf220b7d88fa38ac53698",
+ "https://bcr.bazel.build/modules/rules_nodejs/6.7.5/MODULE.bazel": "97e6794043821d23c013baa4a50fd1c599f2e6ae92b06e2c5f1cd7074fd83e7c",
+ "https://bcr.bazel.build/modules/rules_nodejs/6.7.5/source.json": "d60ee5a76258b1c8f99545ed24172b44d43ba64ca1a2dfc04371ef203df19fdf",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
@@ -336,49 +311,31 @@
},
"selectedYankedVersions": {},
"moduleExtensions": {
- "@@aspect_rules_ts+//ts:extensions.bzl%ext": {
- "general": {
- "bzlTransitiveDigest": "znGIWhsWB2BLftYOmjVGiqrZuccFABBogiS+i4Kg3tU=",
- "usagesDigest": "wmTY4QJrwyD3VUSOHaA0OPkvX0ywR/XPM2EecaJ8yGk=",
- "recordedInputs": [
- "REPO_MAPPING:aspect_rules_ts+,aspect_rules_ts aspect_rules_ts+",
- "REPO_MAPPING:aspect_rules_ts+,bazel_tools bazel_tools",
- "FILE:@@//package.json f230bdb8c76c59a925ec0727a1decc727388d47d4b20d95b4e5bcf328edc0c79"
- ],
- "generatedRepoSpecs": {
- "npm_typescript": {
- "repoRuleId": "@@aspect_rules_ts+//ts/private:npm_repositories.bzl%http_archive_version",
- "attributes": {
- "version": "",
- "version_from": "@@//:package.json",
- "integrity": "",
- "urls": [
- "https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
- ]
- }
- }
- }
- }
- },
"@@aspect_tools_telemetry+//:extension.bzl%telemetry": {
"general": {
- "bzlTransitiveDigest": "cl5A2O84vDL6Tt+Qga8FCj1DUDGqn+e7ly5rZ+4xvcc=",
- "usagesDigest": "VyFtm1DFYJGrU6mU+xj6UF6QCpzpFzBONF0/8BC6ZC8=",
+ "bzlTransitiveDigest": "4w9RM0xjdKo1crk5zL20a/TuhqO0P1z1LsuXDneBXD4=",
+ "usagesDigest": "VuWbxUOa4PEE7KktNjLNLRMFWZRv30gglkSl7mxZj9w=",
"recordedInputs": [
"REPO_MAPPING:aspect_tools_telemetry+,bazel_lib bazel_lib+",
- "REPO_MAPPING:aspect_tools_telemetry+,bazel_skylib bazel_skylib+"
+ "REPO_MAPPING:aspect_tools_telemetry+,bazel_skylib bazel_skylib+",
+ "ENV:ASPECT_TOOLS_TELEMETRY_TEST \\0"
],
"generatedRepoSpecs": {
"aspect_tools_telemetry_report": {
"repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository",
"attributes": {
"deps": {
- "aspect_rules_js": "3.1.2",
- "aspect_rules_ts": "3.8.8",
- "aspect_tools_telemetry": "0.3.3"
- }
+ "aspect_rules_js": "3.2.3",
+ "aspect_rules_ts": "3.9.2",
+ "aspect_tools_telemetry": "0.4.2"
+ },
+ "last_notice": 1
}
}
+ },
+ "moduleExtensionMetadata": {
+ "useAllRepos": "NO",
+ "reproducible": false
}
}
},
@@ -474,425 +431,6 @@
}
}
},
- "@@rules_nodejs+//nodejs:extensions.bzl%node": {
- "general": {
- "bzlTransitiveDigest": "oZFClfRhTTwsYzpxVPkOpOt/r0+OzEfEV37au0jFZ0s=",
- "usagesDigest": "rmuj3zuYtKpBbldwwQvwLpB0VwZVHNwpx5rco2PvuLY=",
- "recordedInputs": [],
- "generatedRepoSpecs": {
- "nodejs_linux_amd64": {
- "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories",
- "attributes": {
- "node_download_auth": {},
- "node_repositories": {
- "24.14.1-darwin_arm64": [
- "node-v24.14.1-darwin-arm64.tar.gz",
- "node-v24.14.1-darwin-arm64",
- "25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe"
- ],
- "24.14.1-darwin_amd64": [
- "node-v24.14.1-darwin-x64.tar.gz",
- "node-v24.14.1-darwin-x64",
- "2526230ad7d922be82d4fdb1e7ee1e84303e133e3b4b0ec4c2897ab31de0253d"
- ],
- "24.14.1-linux_arm64": [
- "node-v24.14.1-linux-arm64.tar.xz",
- "node-v24.14.1-linux-arm64",
- "71e427e28b78846f201d4d5ecc30cb13d1508ca099ef3871889a1256c7d6f67e"
- ],
- "24.14.1-linux_ppc64le": [
- "node-v24.14.1-linux-ppc64le.tar.xz",
- "node-v24.14.1-linux-ppc64le",
- "95bf0c8dbb73144edb79a57399f03c70af6995b78e1c632926e53e6404662ef5"
- ],
- "24.14.1-linux_s390x": [
- "node-v24.14.1-linux-s390x.tar.xz",
- "node-v24.14.1-linux-s390x",
- "ed3bfbc0ff418b0ec4633f23d53a12a691717a34b041c3fbdb296c8774e5a98a"
- ],
- "24.14.1-linux_amd64": [
- "node-v24.14.1-linux-x64.tar.xz",
- "node-v24.14.1-linux-x64",
- "84d38715d449447117d05c3e71acd78daa49d5b1bfa8aacf610303920c3322be"
- ],
- "24.14.1-windows_amd64": [
- "node-v24.14.1-win-x64.zip",
- "node-v24.14.1-win-x64",
- "6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70"
- ]
- },
- "node_urls": [
- "https://nodejs.org/dist/v{version}/{filename}"
- ],
- "node_version": "24.14.1",
- "include_headers": false,
- "platform": "linux_amd64"
- }
- },
- "nodejs_linux_arm64": {
- "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories",
- "attributes": {
- "node_download_auth": {},
- "node_repositories": {
- "24.14.1-darwin_arm64": [
- "node-v24.14.1-darwin-arm64.tar.gz",
- "node-v24.14.1-darwin-arm64",
- "25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe"
- ],
- "24.14.1-darwin_amd64": [
- "node-v24.14.1-darwin-x64.tar.gz",
- "node-v24.14.1-darwin-x64",
- "2526230ad7d922be82d4fdb1e7ee1e84303e133e3b4b0ec4c2897ab31de0253d"
- ],
- "24.14.1-linux_arm64": [
- "node-v24.14.1-linux-arm64.tar.xz",
- "node-v24.14.1-linux-arm64",
- "71e427e28b78846f201d4d5ecc30cb13d1508ca099ef3871889a1256c7d6f67e"
- ],
- "24.14.1-linux_ppc64le": [
- "node-v24.14.1-linux-ppc64le.tar.xz",
- "node-v24.14.1-linux-ppc64le",
- "95bf0c8dbb73144edb79a57399f03c70af6995b78e1c632926e53e6404662ef5"
- ],
- "24.14.1-linux_s390x": [
- "node-v24.14.1-linux-s390x.tar.xz",
- "node-v24.14.1-linux-s390x",
- "ed3bfbc0ff418b0ec4633f23d53a12a691717a34b041c3fbdb296c8774e5a98a"
- ],
- "24.14.1-linux_amd64": [
- "node-v24.14.1-linux-x64.tar.xz",
- "node-v24.14.1-linux-x64",
- "84d38715d449447117d05c3e71acd78daa49d5b1bfa8aacf610303920c3322be"
- ],
- "24.14.1-windows_amd64": [
- "node-v24.14.1-win-x64.zip",
- "node-v24.14.1-win-x64",
- "6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70"
- ]
- },
- "node_urls": [
- "https://nodejs.org/dist/v{version}/{filename}"
- ],
- "node_version": "24.14.1",
- "include_headers": false,
- "platform": "linux_arm64"
- }
- },
- "nodejs_linux_s390x": {
- "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories",
- "attributes": {
- "node_download_auth": {},
- "node_repositories": {
- "24.14.1-darwin_arm64": [
- "node-v24.14.1-darwin-arm64.tar.gz",
- "node-v24.14.1-darwin-arm64",
- "25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe"
- ],
- "24.14.1-darwin_amd64": [
- "node-v24.14.1-darwin-x64.tar.gz",
- "node-v24.14.1-darwin-x64",
- "2526230ad7d922be82d4fdb1e7ee1e84303e133e3b4b0ec4c2897ab31de0253d"
- ],
- "24.14.1-linux_arm64": [
- "node-v24.14.1-linux-arm64.tar.xz",
- "node-v24.14.1-linux-arm64",
- "71e427e28b78846f201d4d5ecc30cb13d1508ca099ef3871889a1256c7d6f67e"
- ],
- "24.14.1-linux_ppc64le": [
- "node-v24.14.1-linux-ppc64le.tar.xz",
- "node-v24.14.1-linux-ppc64le",
- "95bf0c8dbb73144edb79a57399f03c70af6995b78e1c632926e53e6404662ef5"
- ],
- "24.14.1-linux_s390x": [
- "node-v24.14.1-linux-s390x.tar.xz",
- "node-v24.14.1-linux-s390x",
- "ed3bfbc0ff418b0ec4633f23d53a12a691717a34b041c3fbdb296c8774e5a98a"
- ],
- "24.14.1-linux_amd64": [
- "node-v24.14.1-linux-x64.tar.xz",
- "node-v24.14.1-linux-x64",
- "84d38715d449447117d05c3e71acd78daa49d5b1bfa8aacf610303920c3322be"
- ],
- "24.14.1-windows_amd64": [
- "node-v24.14.1-win-x64.zip",
- "node-v24.14.1-win-x64",
- "6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70"
- ]
- },
- "node_urls": [
- "https://nodejs.org/dist/v{version}/{filename}"
- ],
- "node_version": "24.14.1",
- "include_headers": false,
- "platform": "linux_s390x"
- }
- },
- "nodejs_linux_ppc64le": {
- "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories",
- "attributes": {
- "node_download_auth": {},
- "node_repositories": {
- "24.14.1-darwin_arm64": [
- "node-v24.14.1-darwin-arm64.tar.gz",
- "node-v24.14.1-darwin-arm64",
- "25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe"
- ],
- "24.14.1-darwin_amd64": [
- "node-v24.14.1-darwin-x64.tar.gz",
- "node-v24.14.1-darwin-x64",
- "2526230ad7d922be82d4fdb1e7ee1e84303e133e3b4b0ec4c2897ab31de0253d"
- ],
- "24.14.1-linux_arm64": [
- "node-v24.14.1-linux-arm64.tar.xz",
- "node-v24.14.1-linux-arm64",
- "71e427e28b78846f201d4d5ecc30cb13d1508ca099ef3871889a1256c7d6f67e"
- ],
- "24.14.1-linux_ppc64le": [
- "node-v24.14.1-linux-ppc64le.tar.xz",
- "node-v24.14.1-linux-ppc64le",
- "95bf0c8dbb73144edb79a57399f03c70af6995b78e1c632926e53e6404662ef5"
- ],
- "24.14.1-linux_s390x": [
- "node-v24.14.1-linux-s390x.tar.xz",
- "node-v24.14.1-linux-s390x",
- "ed3bfbc0ff418b0ec4633f23d53a12a691717a34b041c3fbdb296c8774e5a98a"
- ],
- "24.14.1-linux_amd64": [
- "node-v24.14.1-linux-x64.tar.xz",
- "node-v24.14.1-linux-x64",
- "84d38715d449447117d05c3e71acd78daa49d5b1bfa8aacf610303920c3322be"
- ],
- "24.14.1-windows_amd64": [
- "node-v24.14.1-win-x64.zip",
- "node-v24.14.1-win-x64",
- "6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70"
- ]
- },
- "node_urls": [
- "https://nodejs.org/dist/v{version}/{filename}"
- ],
- "node_version": "24.14.1",
- "include_headers": false,
- "platform": "linux_ppc64le"
- }
- },
- "nodejs_darwin_amd64": {
- "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories",
- "attributes": {
- "node_download_auth": {},
- "node_repositories": {
- "24.14.1-darwin_arm64": [
- "node-v24.14.1-darwin-arm64.tar.gz",
- "node-v24.14.1-darwin-arm64",
- "25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe"
- ],
- "24.14.1-darwin_amd64": [
- "node-v24.14.1-darwin-x64.tar.gz",
- "node-v24.14.1-darwin-x64",
- "2526230ad7d922be82d4fdb1e7ee1e84303e133e3b4b0ec4c2897ab31de0253d"
- ],
- "24.14.1-linux_arm64": [
- "node-v24.14.1-linux-arm64.tar.xz",
- "node-v24.14.1-linux-arm64",
- "71e427e28b78846f201d4d5ecc30cb13d1508ca099ef3871889a1256c7d6f67e"
- ],
- "24.14.1-linux_ppc64le": [
- "node-v24.14.1-linux-ppc64le.tar.xz",
- "node-v24.14.1-linux-ppc64le",
- "95bf0c8dbb73144edb79a57399f03c70af6995b78e1c632926e53e6404662ef5"
- ],
- "24.14.1-linux_s390x": [
- "node-v24.14.1-linux-s390x.tar.xz",
- "node-v24.14.1-linux-s390x",
- "ed3bfbc0ff418b0ec4633f23d53a12a691717a34b041c3fbdb296c8774e5a98a"
- ],
- "24.14.1-linux_amd64": [
- "node-v24.14.1-linux-x64.tar.xz",
- "node-v24.14.1-linux-x64",
- "84d38715d449447117d05c3e71acd78daa49d5b1bfa8aacf610303920c3322be"
- ],
- "24.14.1-windows_amd64": [
- "node-v24.14.1-win-x64.zip",
- "node-v24.14.1-win-x64",
- "6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70"
- ]
- },
- "node_urls": [
- "https://nodejs.org/dist/v{version}/{filename}"
- ],
- "node_version": "24.14.1",
- "include_headers": false,
- "platform": "darwin_amd64"
- }
- },
- "nodejs_darwin_arm64": {
- "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories",
- "attributes": {
- "node_download_auth": {},
- "node_repositories": {
- "24.14.1-darwin_arm64": [
- "node-v24.14.1-darwin-arm64.tar.gz",
- "node-v24.14.1-darwin-arm64",
- "25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe"
- ],
- "24.14.1-darwin_amd64": [
- "node-v24.14.1-darwin-x64.tar.gz",
- "node-v24.14.1-darwin-x64",
- "2526230ad7d922be82d4fdb1e7ee1e84303e133e3b4b0ec4c2897ab31de0253d"
- ],
- "24.14.1-linux_arm64": [
- "node-v24.14.1-linux-arm64.tar.xz",
- "node-v24.14.1-linux-arm64",
- "71e427e28b78846f201d4d5ecc30cb13d1508ca099ef3871889a1256c7d6f67e"
- ],
- "24.14.1-linux_ppc64le": [
- "node-v24.14.1-linux-ppc64le.tar.xz",
- "node-v24.14.1-linux-ppc64le",
- "95bf0c8dbb73144edb79a57399f03c70af6995b78e1c632926e53e6404662ef5"
- ],
- "24.14.1-linux_s390x": [
- "node-v24.14.1-linux-s390x.tar.xz",
- "node-v24.14.1-linux-s390x",
- "ed3bfbc0ff418b0ec4633f23d53a12a691717a34b041c3fbdb296c8774e5a98a"
- ],
- "24.14.1-linux_amd64": [
- "node-v24.14.1-linux-x64.tar.xz",
- "node-v24.14.1-linux-x64",
- "84d38715d449447117d05c3e71acd78daa49d5b1bfa8aacf610303920c3322be"
- ],
- "24.14.1-windows_amd64": [
- "node-v24.14.1-win-x64.zip",
- "node-v24.14.1-win-x64",
- "6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70"
- ]
- },
- "node_urls": [
- "https://nodejs.org/dist/v{version}/{filename}"
- ],
- "node_version": "24.14.1",
- "include_headers": false,
- "platform": "darwin_arm64"
- }
- },
- "nodejs_windows_amd64": {
- "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories",
- "attributes": {
- "node_download_auth": {},
- "node_repositories": {
- "24.14.1-darwin_arm64": [
- "node-v24.14.1-darwin-arm64.tar.gz",
- "node-v24.14.1-darwin-arm64",
- "25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe"
- ],
- "24.14.1-darwin_amd64": [
- "node-v24.14.1-darwin-x64.tar.gz",
- "node-v24.14.1-darwin-x64",
- "2526230ad7d922be82d4fdb1e7ee1e84303e133e3b4b0ec4c2897ab31de0253d"
- ],
- "24.14.1-linux_arm64": [
- "node-v24.14.1-linux-arm64.tar.xz",
- "node-v24.14.1-linux-arm64",
- "71e427e28b78846f201d4d5ecc30cb13d1508ca099ef3871889a1256c7d6f67e"
- ],
- "24.14.1-linux_ppc64le": [
- "node-v24.14.1-linux-ppc64le.tar.xz",
- "node-v24.14.1-linux-ppc64le",
- "95bf0c8dbb73144edb79a57399f03c70af6995b78e1c632926e53e6404662ef5"
- ],
- "24.14.1-linux_s390x": [
- "node-v24.14.1-linux-s390x.tar.xz",
- "node-v24.14.1-linux-s390x",
- "ed3bfbc0ff418b0ec4633f23d53a12a691717a34b041c3fbdb296c8774e5a98a"
- ],
- "24.14.1-linux_amd64": [
- "node-v24.14.1-linux-x64.tar.xz",
- "node-v24.14.1-linux-x64",
- "84d38715d449447117d05c3e71acd78daa49d5b1bfa8aacf610303920c3322be"
- ],
- "24.14.1-windows_amd64": [
- "node-v24.14.1-win-x64.zip",
- "node-v24.14.1-win-x64",
- "6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70"
- ]
- },
- "node_urls": [
- "https://nodejs.org/dist/v{version}/{filename}"
- ],
- "node_version": "24.14.1",
- "include_headers": false,
- "platform": "windows_amd64"
- }
- },
- "nodejs_windows_arm64": {
- "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories",
- "attributes": {
- "node_download_auth": {},
- "node_repositories": {
- "24.14.1-darwin_arm64": [
- "node-v24.14.1-darwin-arm64.tar.gz",
- "node-v24.14.1-darwin-arm64",
- "25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe"
- ],
- "24.14.1-darwin_amd64": [
- "node-v24.14.1-darwin-x64.tar.gz",
- "node-v24.14.1-darwin-x64",
- "2526230ad7d922be82d4fdb1e7ee1e84303e133e3b4b0ec4c2897ab31de0253d"
- ],
- "24.14.1-linux_arm64": [
- "node-v24.14.1-linux-arm64.tar.xz",
- "node-v24.14.1-linux-arm64",
- "71e427e28b78846f201d4d5ecc30cb13d1508ca099ef3871889a1256c7d6f67e"
- ],
- "24.14.1-linux_ppc64le": [
- "node-v24.14.1-linux-ppc64le.tar.xz",
- "node-v24.14.1-linux-ppc64le",
- "95bf0c8dbb73144edb79a57399f03c70af6995b78e1c632926e53e6404662ef5"
- ],
- "24.14.1-linux_s390x": [
- "node-v24.14.1-linux-s390x.tar.xz",
- "node-v24.14.1-linux-s390x",
- "ed3bfbc0ff418b0ec4633f23d53a12a691717a34b041c3fbdb296c8774e5a98a"
- ],
- "24.14.1-linux_amd64": [
- "node-v24.14.1-linux-x64.tar.xz",
- "node-v24.14.1-linux-x64",
- "84d38715d449447117d05c3e71acd78daa49d5b1bfa8aacf610303920c3322be"
- ],
- "24.14.1-windows_amd64": [
- "node-v24.14.1-win-x64.zip",
- "node-v24.14.1-win-x64",
- "6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70"
- ]
- },
- "node_urls": [
- "https://nodejs.org/dist/v{version}/{filename}"
- ],
- "node_version": "24.14.1",
- "include_headers": false,
- "platform": "windows_arm64"
- }
- },
- "nodejs": {
- "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias",
- "attributes": {
- "user_node_repository_name": "nodejs"
- }
- },
- "nodejs_host": {
- "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias",
- "attributes": {
- "user_node_repository_name": "nodejs"
- }
- },
- "nodejs_toolchains": {
- "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_toolchains_repo.bzl%nodejs_toolchains_repo",
- "attributes": {
- "user_node_repository_name": "nodejs"
- }
- }
- }
- }
- },
"@@rules_python+//python/uv:uv.bzl%uv": {
"general": {
"bzlTransitiveDigest": "I8FPZMevE2oI/peSpMBRVIN++WOtfjtJVjbPsBZQ87A=",
@@ -1287,7 +825,7 @@
"@@yq.bzl+//yq:extensions.bzl%yq": {
"general": {
"bzlTransitiveDigest": "tDqk+ntWTdxNAWPDjRY1uITgHbti2jcXR5ZdinltBs0=",
- "usagesDigest": "Pz33iK5QtkPFivaljfReN4KzpAqM2DgY4vwgyZSNoAM=",
+ "usagesDigest": "XGLRpNcVs4WD/zog6U0sXbBo2OlR5O4mc58OU1L3EVc=",
"recordedInputs": [],
"generatedRepoSpecs": {
"yq_darwin_amd64": {
@@ -1364,6 +902,9 @@
}
},
"facts": {
+ "@@aspect_tools_telemetry+//:extension.bzl%telemetry": {
+ "notice_version": "1"
+ },
"@@rules_go+//go:extensions.bzl%go_sdk": {
"1.22.4": {
"aix_ppc64": [
diff --git a/configs/agent_configs.textproto b/configs/agent_configs.textproto
index 08f2602..c16bf16b 100644
--- a/configs/agent_configs.textproto
+++ b/configs/agent_configs.textproto
@@ -46,7 +46,6 @@
project: "gerrit"
path_regex: "^/?COMMIT_MSG$"
}
- automatic: true
}
# TypeScript Style Review Agent
@@ -60,5 +59,6 @@
project: "TestRepo"
path_regex: ".*\\.ts$"
}
+ automatic: true
}
diff --git a/contrib/maintenance/gerrit/site.py b/contrib/maintenance/gerrit/site.py
index faf6c02..450acaa 100644
--- a/contrib/maintenance/gerrit/site.py
+++ b/contrib/maintenance/gerrit/site.py
@@ -37,11 +37,11 @@
) as cfg:
config_base_path = cfg.get("gerrit", None, "basePath", "git")
if os.path.isabs(config_base_path):
- self.basePath = config_base_path
+ self.base_path = config_base_path
else:
- self.basePath = os.path.join(self.path, config_base_path)
+ self.base_path = os.path.join(self.path, config_base_path)
- return self.basePath
+ return self.base_path
def get_projects(self, excludes=None):
for current, dirs, _ in os.walk(self.get_base_path(), topdown=True):
diff --git a/external_deps.lock.json b/external_deps.lock.json
index b4b538d..141bbfc 100644
--- a/external_deps.lock.json
+++ b/external_deps.lock.json
@@ -8,10 +8,10 @@
"com.github.ben-manes.caffeine:caffeine": 1735183231,
"com.github.ben-manes.caffeine:guava": -2131703186,
"com.github.rholder:guava-retrying": 1309513165,
- "com.google.auto.factory:auto-factory": -1233360965,
- "com.google.auto.service:auto-service-annotations": 953830824,
- "com.google.auto.value:auto-value": 1146221104,
- "com.google.auto.value:auto-value-annotations": -1101276935,
+ "com.google.auto.factory:auto-factory": -1222857861,
+ "com.google.auto.service:auto-service-annotations": 964333928,
+ "com.google.auto.value:auto-value": -661233359,
+ "com.google.auto.value:auto-value-annotations": 1386235898,
"com.google.auto:auto-common": -832702775,
"com.google.code.findbugs:jsr305": -1992157670,
"com.google.code.gson:gson": 2063056812,
@@ -28,13 +28,13 @@
"com.google.guava:failureaccess": -2032498474,
"com.google.guava:guava": -1756621521,
"com.google.guava:guava-testlib": -203887467,
- "com.google.inject.extensions:guice-assistedinject": -1667539622,
- "com.google.inject.extensions:guice-servlet": 569202692,
- "com.google.inject:guice": -1660789120,
+ "com.google.inject.extensions:guice-assistedinject": -1742632647,
+ "com.google.inject.extensions:guice-servlet": -1284557873,
+ "com.google.inject:guice": 2106361664,
"com.google.j2objc:j2objc-annotations": -727464895,
"com.google.jimfs:jimfs": -1004381565,
"com.google.protobuf:protobuf-java": 1247529435,
- "com.google.template:soy": -1478719887,
+ "com.google.template:soy": -843524660,
"com.google.truth.extensions:truth-java8-extension": -129319374,
"com.google.truth.extensions:truth-liteproto-extension": 1463279446,
"com.google.truth.extensions:truth-proto-extension": 1270333764,
@@ -109,7 +109,7 @@
"commons-dbcp:commons-dbcp": -873877417,
"commons-digester:commons-digester": 1688456634,
"commons-io:commons-io": 1305681826,
- "commons-logging:commons-logging": 243315756,
+ "commons-logging:commons-logging": -264381874,
"commons-net:commons-net": 1227155931,
"commons-pool:commons-pool": -2015226625,
"commons-validator:commons-validator": -89255997,
@@ -119,16 +119,17 @@
"io.github.java-diff-utils:java-diff-utils": 1296992014,
"io.sweers.autotransient:autotransient": 1591678970,
"jakarta.inject:jakarta.inject-api": 190564362,
+ "jakarta.servlet:jakarta.servlet-api": -3550902,
"javax.activation:activation": -940616209,
"javax.annotation:jsr250-api": -593155132,
"javax.inject:javax.inject": -297932879,
"javax.servlet:javax.servlet-api": 669233360,
"junit:junit": -744267592,
"log4j:log4j": 182326902,
- "net.bytebuddy:byte-buddy": -67913315,
- "net.bytebuddy:byte-buddy-agent": -728183915,
- "net.java.dev.jna:jna": 939544101,
- "net.java.dev.jna:jna-platform": 1246142177,
+ "net.bytebuddy:byte-buddy": -1875367778,
+ "net.bytebuddy:byte-buddy-agent": 1759328918,
+ "net.java.dev.jna:jna": -867910362,
+ "net.java.dev.jna:jna-platform": -561312286,
"net.minidev:json-smart": -1043043954,
"net.sf.jopt-simple:jopt-simple": 906822697,
"net.sourceforge.nekohtml:nekohtml": 1723624706,
@@ -152,12 +153,12 @@
"org.apache.lucene:lucene-queries": 1341801569,
"org.apache.lucene:lucene-queryparser": 2090205297,
"org.apache.lucene:lucene-sandbox": 1803509280,
- "org.apache.mina:mina-core": -955969490,
+ "org.apache.mina:mina-core": -1403307213,
"org.apache.sshd:sshd-common": 1844247612,
"org.apache.sshd:sshd-core": 602454664,
- "org.apache.sshd:sshd-mina": -1915268089,
- "org.apache.sshd:sshd-osgi": 1681538032,
- "org.apache.sshd:sshd-sftp": 2089761863,
+ "org.apache.sshd:sshd-mina": 582747848,
+ "org.apache.sshd:sshd-osgi": -115413327,
+ "org.apache.sshd:sshd-sftp": 292810504,
"org.asciidoctor:asciidoctorj": -457860213,
"org.assertj:assertj-core": -1145412507,
"org.bouncycastle:bcpg-jdk18on": -1572213535,
@@ -168,17 +169,18 @@
"org.commonmark:commonmark-ext-autolink": -1853742120,
"org.commonmark:commonmark-ext-gfm-strikethrough": 350394231,
"org.commonmark:commonmark-ext-gfm-tables": 1881582931,
- "org.eclipse.jetty.ee8:jetty-ee8-nested": -1627496831,
- "org.eclipse.jetty.ee8:jetty-ee8-security": 1632270986,
- "org.eclipse.jetty.ee8:jetty-ee8-servlet": -1338697711,
- "org.eclipse.jetty:jetty-http": -235909997,
- "org.eclipse.jetty:jetty-io": -44878927,
- "org.eclipse.jetty:jetty-jmx": 1781043564,
- "org.eclipse.jetty:jetty-security": -1819173301,
- "org.eclipse.jetty:jetty-server": -130172210,
- "org.eclipse.jetty:jetty-session": -1378386067,
- "org.eclipse.jetty:jetty-util": -444622835,
- "org.eclipse.jetty:jetty-util-ajax": -622085534,
+ "org.eclipse.jetty.ee11:jetty-ee11-servlet": -562048764,
+ "org.eclipse.jetty.ee8:jetty-ee8-nested": 860016002,
+ "org.eclipse.jetty.ee8:jetty-ee8-security": -175183477,
+ "org.eclipse.jetty.ee8:jetty-ee8-servlet": 1148815122,
+ "org.eclipse.jetty:jetty-http": -2043364460,
+ "org.eclipse.jetty:jetty-io": -1852333390,
+ "org.eclipse.jetty:jetty-jmx": -26410899,
+ "org.eclipse.jetty:jetty-security": 668339532,
+ "org.eclipse.jetty:jetty-server": -1937626673,
+ "org.eclipse.jetty:jetty-session": 1109126766,
+ "org.eclipse.jetty:jetty-util": 2042889998,
+ "org.eclipse.jetty:jetty-util-ajax": 1865427299,
"org.hamcrest:hamcrest": 1547523135,
"org.jruby:jruby-complete": -2103568068,
"org.json:json": -811907600,
@@ -186,7 +188,7 @@
"org.mockito:mockito-core": 1330163800,
"org.nibor.autolink:autolink": -342487050,
"org.objenesis:objenesis": 748376655,
- "org.openid4java:openid4java": -842286787,
+ "org.openid4java:openid4java": -1889179110,
"org.openjdk.jmh:jmh-core": 983716932,
"org.openjdk.jmh:jmh-generator-annprocess": -1162360421,
"org.ow2.asm:asm": 1206815935,
@@ -223,14 +225,14 @@
"com.github.ben-manes.caffeine:guava:jar:sources": 1590068668,
"com.github.rholder:guava-retrying": -789983219,
"com.github.rholder:guava-retrying:jar:sources": 2030887181,
- "com.google.auto.factory:auto-factory": 1657393907,
- "com.google.auto.factory:auto-factory:jar:sources": 1355400808,
- "com.google.auto.service:auto-service-annotations": 1999106941,
- "com.google.auto.service:auto-service-annotations:jar:sources": 1716120839,
- "com.google.auto.value:auto-value": 1110951021,
- "com.google.auto.value:auto-value-annotations": 641018093,
- "com.google.auto.value:auto-value-annotations:jar:sources": -315473772,
- "com.google.auto.value:auto-value:jar:sources": 1982175049,
+ "com.google.auto.factory:auto-factory": 2025609377,
+ "com.google.auto.factory:auto-factory:jar:sources": -861333607,
+ "com.google.auto.service:auto-service-annotations": 195984873,
+ "com.google.auto.service:auto-service-annotations:jar:sources": -902556490,
+ "com.google.auto.value:auto-value": -1504415167,
+ "com.google.auto.value:auto-value-annotations": 641752776,
+ "com.google.auto.value:auto-value-annotations:jar:sources": -2083311588,
+ "com.google.auto.value:auto-value:jar:sources": -30292358,
"com.google.auto:auto-common": -195836273,
"com.google.auto:auto-common:jar:sources": -2100626169,
"com.google.code.findbugs:jsr305": -998441376,
@@ -260,11 +262,11 @@
"com.google.guava:guava-testlib:jar:sources": 1351030420,
"com.google.guava:guava:jar:sources": 1246910673,
"com.google.guava:listenablefuture": 1079558157,
- "com.google.inject.extensions:guice-assistedinject": 773600044,
+ "com.google.inject.extensions:guice-assistedinject": -205980308,
"com.google.inject.extensions:guice-assistedinject:jar:sources": 550961491,
- "com.google.inject.extensions:guice-servlet": 350832471,
+ "com.google.inject.extensions:guice-servlet": -325370690,
"com.google.inject.extensions:guice-servlet:jar:sources": -828654042,
- "com.google.inject:guice": 1105471728,
+ "com.google.inject:guice:jar:classes": 1808670374,
"com.google.inject:guice:jar:sources": -2060215316,
"com.google.j2objc:j2objc-annotations": 1702790440,
"com.google.j2objc:j2objc-annotations:jar:sources": -1254484583,
@@ -274,15 +276,15 @@
"com.google.jsinterop:jsinterop-annotations:jar:sources": 694679492,
"com.google.protobuf:protobuf-java": 1331070778,
"com.google.protobuf:protobuf-java:jar:sources": 133270104,
- "com.google.template:soy": -677841031,
+ "com.google.template:soy": 1170788493,
"com.google.template:soy:jar:sources": 895044971,
- "com.google.truth.extensions:truth-java8-extension": 18757275,
+ "com.google.truth.extensions:truth-java8-extension": 766384514,
"com.google.truth.extensions:truth-java8-extension:jar:sources": 1257445795,
- "com.google.truth.extensions:truth-liteproto-extension": -704876174,
+ "com.google.truth.extensions:truth-liteproto-extension": -1774954418,
"com.google.truth.extensions:truth-liteproto-extension:jar:sources": -754757455,
- "com.google.truth.extensions:truth-proto-extension": 220019054,
+ "com.google.truth.extensions:truth-proto-extension": -180673672,
"com.google.truth.extensions:truth-proto-extension:jar:sources": -171856482,
- "com.google.truth:truth": 341960611,
+ "com.google.truth:truth": 2133252626,
"com.google.truth:truth:jar:sources": 494258718,
"com.googlecode.javaewah:JavaEWAH": 1782327838,
"com.googlecode.javaewah:JavaEWAH:jar:sources": -77762967,
@@ -304,9 +306,9 @@
"com.jcraft:jsch:jar:sources": -586408740,
"com.jcraft:jzlib": -1423321085,
"com.jcraft:jzlib:jar:sources": 1440827148,
- "com.ryanharter.auto.value:auto-value-gson-extension": 550631721,
+ "com.ryanharter.auto.value:auto-value-gson-extension": -1071085858,
"com.ryanharter.auto.value:auto-value-gson-extension:jar:sources": -670606081,
- "com.ryanharter.auto.value:auto-value-gson-factory": 151677491,
+ "com.ryanharter.auto.value:auto-value-gson-factory": -586162268,
"com.ryanharter.auto.value:auto-value-gson-factory:jar:sources": -543638406,
"com.ryanharter.auto.value:auto-value-gson-runtime": 1330169783,
"com.ryanharter.auto.value:auto-value-gson-runtime:jar:sources": 706425522,
@@ -338,6 +340,8 @@
"io.sweers.autotransient:autotransient:jar:sources": -703216994,
"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": 1998212823,
"javax.activation:activation:jar:sources": 2062811499,
"javax.inject:javax.inject": 698155243,
@@ -346,14 +350,14 @@
"javax.servlet:javax.servlet-api:jar:sources": -2015355058,
"junit:junit": -1256429642,
"junit:junit:jar:sources": 940567721,
- "net.bytebuddy:byte-buddy": -645553046,
- "net.bytebuddy:byte-buddy-agent": 567350281,
- "net.bytebuddy:byte-buddy-agent:jar:sources": 467108684,
- "net.bytebuddy:byte-buddy:jar:sources": -315779909,
- "net.java.dev.jna:jna": 1254948548,
- "net.java.dev.jna:jna-platform": 653780437,
- "net.java.dev.jna:jna-platform:jar:sources": 194874308,
- "net.java.dev.jna:jna:jar:sources": -1685556831,
+ "net.bytebuddy:byte-buddy": -2144538556,
+ "net.bytebuddy:byte-buddy-agent": -1307562154,
+ "net.bytebuddy:byte-buddy-agent:jar:sources": 2038081020,
+ "net.bytebuddy:byte-buddy:jar:sources": 1602176187,
+ "net.java.dev.jna:jna": 1622514527,
+ "net.java.dev.jna:jna-platform": 1756885266,
+ "net.java.dev.jna:jna-platform:jar:sources": -507721531,
+ "net.java.dev.jna:jna:jar:sources": -902150228,
"net.minidev:json-smart": 1099098947,
"net.minidev:json-smart:jar:sources": 1458315043,
"net.sf.jopt-simple:jopt-simple": -1677351973,
@@ -396,17 +400,17 @@
"org.apache.lucene:lucene-misc:jar:sources": 1204085693,
"org.apache.lucene:lucene-queryparser": 2111539382,
"org.apache.lucene:lucene-queryparser:jar:sources": 1992621418,
- "org.apache.mina:mina-core": 314726127,
- "org.apache.mina:mina-core:jar:sources": 758815578,
- "org.apache.sshd:sshd-mina": -734078396,
- "org.apache.sshd:sshd-mina:jar:sources": -506380854,
- "org.apache.sshd:sshd-osgi": 1092773821,
- "org.apache.sshd:sshd-osgi:jar:sources": -2061003935,
- "org.apache.sshd:sshd-sftp": -70173235,
- "org.apache.sshd:sshd-sftp:jar:sources": 1034555480,
+ "org.apache.mina:mina-core": -584591076,
+ "org.apache.mina:mina-core:jar:sources": 71382131,
+ "org.apache.sshd:sshd-mina": -36716477,
+ "org.apache.sshd:sshd-mina:jar:sources": 97528844,
+ "org.apache.sshd:sshd-osgi": 1810958617,
+ "org.apache.sshd:sshd-osgi:jar:sources": 710209222,
+ "org.apache.sshd:sshd-sftp": -320832840,
+ "org.apache.sshd:sshd-sftp:jar:sources": -1268961386,
"org.asciidoctor:asciidoctorj": 1685789893,
"org.asciidoctor:asciidoctorj:jar:sources": 2091708864,
- "org.assertj:assertj-core": -923964809,
+ "org.assertj:assertj-core": -59722598,
"org.assertj:assertj-core:jar:sources": -697161745,
"org.bouncycastle:bcpg-jdk18on": -1258294405,
"org.bouncycastle:bcpg-jdk18on:jar:sources": -1733408473,
@@ -428,30 +432,32 @@
"org.commonmark:commonmark-ext-gfm-tables": -1205584749,
"org.commonmark:commonmark-ext-gfm-tables:jar:sources": 1341057091,
"org.commonmark:commonmark:jar:sources": -1511261547,
- "org.eclipse.jetty.ee8:jetty-ee8-nested": -1480285467,
- "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources": -2141029892,
- "org.eclipse.jetty.ee8:jetty-ee8-security": -1089777429,
- "org.eclipse.jetty.ee8:jetty-ee8-security:jar:sources": -374234244,
- "org.eclipse.jetty.ee8:jetty-ee8-servlet": 709534403,
- "org.eclipse.jetty.ee8:jetty-ee8-servlet:jar:sources": 1796299448,
+ "org.eclipse.jetty.ee11:jetty-ee11-servlet": -2443254,
+ "org.eclipse.jetty.ee11:jetty-ee11-servlet:jar:sources": -1546113901,
+ "org.eclipse.jetty.ee8:jetty-ee8-nested": -1915352225,
+ "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources": 673934800,
+ "org.eclipse.jetty.ee8:jetty-ee8-security": -1918552702,
+ "org.eclipse.jetty.ee8:jetty-ee8-security:jar:sources": 114520187,
+ "org.eclipse.jetty.ee8:jetty-ee8-servlet": 57123647,
+ "org.eclipse.jetty.ee8:jetty-ee8-servlet:jar:sources": 500172952,
"org.eclipse.jetty.toolchain:jetty-servlet-api": 1364182673,
"org.eclipse.jetty.toolchain:jetty-servlet-api:jar:sources": 736604807,
- "org.eclipse.jetty:jetty-http": -1578822522,
- "org.eclipse.jetty:jetty-http:jar:sources": -1317713476,
- "org.eclipse.jetty:jetty-io": -225847225,
- "org.eclipse.jetty:jetty-io:jar:sources": 116677681,
- "org.eclipse.jetty:jetty-jmx": 1202016563,
- "org.eclipse.jetty:jetty-jmx:jar:sources": -1552312519,
- "org.eclipse.jetty:jetty-security": 501899561,
- "org.eclipse.jetty:jetty-security:jar:sources": 1985223928,
- "org.eclipse.jetty:jetty-server": -963731471,
- "org.eclipse.jetty:jetty-server:jar:sources": -466994191,
- "org.eclipse.jetty:jetty-session": 1869188771,
- "org.eclipse.jetty:jetty-session:jar:sources": -1630590830,
- "org.eclipse.jetty:jetty-util": -1027747345,
- "org.eclipse.jetty:jetty-util-ajax": 573093314,
- "org.eclipse.jetty:jetty-util-ajax:jar:sources": 455294676,
- "org.eclipse.jetty:jetty-util:jar:sources": -280937557,
+ "org.eclipse.jetty:jetty-http": -2005285297,
+ "org.eclipse.jetty:jetty-http:jar:sources": 1510111824,
+ "org.eclipse.jetty:jetty-io": 1963881320,
+ "org.eclipse.jetty:jetty-io:jar:sources": -219635344,
+ "org.eclipse.jetty:jetty-jmx": 974624101,
+ "org.eclipse.jetty:jetty-jmx:jar:sources": -733987429,
+ "org.eclipse.jetty:jetty-security": -628512589,
+ "org.eclipse.jetty:jetty-security:jar:sources": 730204298,
+ "org.eclipse.jetty:jetty-server": -1113673878,
+ "org.eclipse.jetty:jetty-server:jar:sources": -1325429404,
+ "org.eclipse.jetty:jetty-session": -240475595,
+ "org.eclipse.jetty:jetty-session:jar:sources": -407525695,
+ "org.eclipse.jetty:jetty-util": 1729944958,
+ "org.eclipse.jetty:jetty-util-ajax": 1312600683,
+ "org.eclipse.jetty:jetty-util-ajax:jar:sources": -2027361402,
+ "org.eclipse.jetty:jetty-util:jar:sources": -1809018264,
"org.hamcrest:hamcrest": 1282317766,
"org.hamcrest:hamcrest-core": 649657847,
"org.hamcrest:hamcrest-core:jar:sources": -1646511374,
@@ -462,13 +468,13 @@
"org.jsoup:jsoup:jar:sources": -2058254438,
"org.jspecify:jspecify": 117231129,
"org.jspecify:jspecify:jar:sources": -2134060298,
- "org.mockito:mockito-core": -287830973,
+ "org.mockito:mockito-core": 493796464,
"org.mockito:mockito-core:jar:sources": 1900207417,
"org.nibor.autolink:autolink": -443901116,
"org.nibor.autolink:autolink:jar:sources": -1863403724,
"org.objenesis:objenesis": 1536526812,
"org.objenesis:objenesis:jar:sources": 321662415,
- "org.openid4java:openid4java": 1109904517,
+ "org.openid4java:openid4java": -469605553,
"org.openid4java:openid4java:jar:sources": -1312563952,
"org.openjdk.jmh:jmh-core": 262505124,
"org.openjdk.jmh:jmh-core:jar:sources": 814651627,
@@ -561,31 +567,31 @@
},
"com.google.auto.factory:auto-factory": {
"shasums": {
- "jar": "d59fb7ada5962a480abf0b81d4d2a14a2952f17c026732359af8b585e531c16c",
- "sources": "c6098f8976b8833cf40edc36d1e3f7cd5cbb474c018185650b5ca4e24e713e6a"
+ "jar": "9b4505cd7a60574d59386672c1d51d6154b803c892677eb909cf4155ebee771f",
+ "sources": "4f46cc87784c2339d103c3b3d4e19bc03dd3ecca018ea931c61c4b63a4829616"
},
- "version": "1.0.1"
+ "version": "1.1.0"
},
"com.google.auto.service:auto-service-annotations": {
"shasums": {
- "jar": "c7bec54b7b5588b5967e870341091c5691181d954cf2039f1bf0a6eeb837473b",
- "sources": "b013ca159b0fea3a0041d3d5fbb3b7e49a819da80a172a01fb17dd28fd98e72b"
+ "jar": "cb5667036f75e9682b493b8b47adbe391bcff72b9d3e16c52ae725f514665af2",
+ "sources": "10ca2b6c041ec8a7dc07d391ab792a55a4d6518d8067c8e63b7deda92e1de9e9"
},
- "version": "1.0.1"
+ "version": "1.1.0"
},
"com.google.auto.value:auto-value": {
"shasums": {
- "jar": "aaf8d637bfed3c420436b9facf1b7a88d12c8785374e4202382783005319c2c3",
- "sources": "4bff06fe077d68f964bd5e05f020ed78fd7870730441e403a2eb306360c4890a"
+ "jar": "0c556370ca3d072ba7dec6b37751ccad0e4804b6f3390cc9acb800d821dd9c0a",
+ "sources": "a9eb12d149bfbcfc7fb670689d67dea3440b0ecaea15829e2953a03a1503b29c"
},
- "version": "1.11.0"
+ "version": "1.11.1"
},
"com.google.auto.value:auto-value-annotations": {
"shasums": {
- "jar": "5a055ce4255333b3346e1a8703da5bf8ff049532286fdcd31712d624abe111dd",
- "sources": "d7941e5f19bb38afcfa85350d57e5245856c23c98c2bbe32f6d31b5577f2bc33"
+ "jar": "6c61a11420a5cddf3313888e9d335d5eedfbeb9a8da26591470fbfe61f5bf859",
+ "sources": "2bc2b5c2f8b1dbe3c05673750f16ec51db2ef9bf54be942c278e16035e5dca21"
},
- "version": "1.11.0"
+ "version": "1.11.1"
},
"com.google.auto:auto-common": {
"shasums": {
@@ -707,7 +713,7 @@
},
"com.google.inject:guice": {
"shasums": {
- "jar": "b4d4f7ec5e8fc17b4f98dee9d3f6cf6ae3ae13e2e5ed4b2f7bbf09bc4bb675d5",
+ "classes": "ff084ab91a2f00745d2535883e4c2e15f40970f93887c2df50504897119cfc3d",
"sources": "656b82a85535ada22d251fbc4ab3e786e66997510d03325d168bc193c2148c09"
},
"version": "6.0.0"
@@ -969,6 +975,13 @@
},
"version": "2.0.1"
},
+ "jakarta.servlet:jakarta.servlet-api": {
+ "shasums": {
+ "jar": "8a31f465f3593bf2351531a5c952014eb839da96a605b5825b93dd54714c48c4",
+ "sources": "6eb958543e0548bb93d2519e40224d13c8003b10cc615b5652bfd9899350bfb4"
+ },
+ "version": "6.1.0"
+ },
"javax.activation:activation": {
"shasums": {
"jar": "ae475120e9fcd99b4b00b38329bd61cdc5eb754eee03fe66c01f50e137724f99",
@@ -999,31 +1012,31 @@
},
"net.bytebuddy:byte-buddy": {
"shasums": {
- "jar": "8b31f4ea806afaa900b67bffd8498760d1f65464f4c2ea78cdbda2f3e633898b",
- "sources": "d40bbc426b8e79a026b4e570e6f57b0a8659b122864c874cfa309121066f1fb1"
+ "jar": "e32f454c2c1f4aca982f9ec764ed892d9a6eee7e8a77f435cbdd180f6ffdb821",
+ "sources": "7f38a50fa7406b61d867a2c81a89610bb816de2fa28696b4a54fcaa8d52530a1"
},
- "version": "1.18.10"
+ "version": "1.18.11"
},
"net.bytebuddy:byte-buddy-agent": {
"shasums": {
- "jar": "9cfa3c71c8f07bfb5f4c3c68db7715940d792801eba82d346fc343f7bd7c2401",
- "sources": "af62bcc1d124110ee1917f61d4cf3c19eb832f9213a555a1d375b743327345a8"
+ "jar": "780b3601c34150ab9fb822673c472f7229c1041e8e2a12facf6a5de9ba7a2d64",
+ "sources": "30bbf5860aa58e4740b352d2c4c15fe2428a2019e81dccd25c8e3937a0a2a0a3"
},
- "version": "1.18.10"
+ "version": "1.18.11"
},
"net.java.dev.jna:jna": {
"shasums": {
- "jar": "f05122f39e2fd8aa1bfbdb2b0306034e2b20dc92817eba119f8c0c2f3399bf3a",
- "sources": "3e0371bec33252b88cede182bd653ea0580063b3ebc1d97dea2122b2639675e7"
+ "jar": "4fb141dd8ef6b0585ffceea4bc49602fbc6312fa977e2c488794ea3e6aafecae",
+ "sources": "0136f719ddd91d92d2368aa2c59a0c436810ac0b9c29024044383758b0e99786"
},
- "version": "5.19.0"
+ "version": "5.19.1"
},
"net.java.dev.jna:jna-platform": {
"shasums": {
- "jar": "44ceeb7a0f3f9c1e74b96ecdce047f8cc85a8c72c491f7662ba64db899559870",
+ "jar": "3b3864f5b449e9c3c24b16861524b622b086563f44e0cd8384c8efc5a6052f82",
"sources": "8288e781424a34b0478928be5664b01ace168e80608178d37d02464041528940"
},
- "version": "5.19.0"
+ "version": "5.19.1"
},
"net.minidev:json-smart": {
"shasums": {
@@ -1174,31 +1187,31 @@
},
"org.apache.mina:mina-core": {
"shasums": {
- "jar": "39b2dfc8e84380bf7adab657d3d5e1625cb6592a885ebdb854ec5c6f7a3ec88d",
- "sources": "6c7823b8ed5a8d3511b8fb7ba6166ab825a45784741cd15c9991a75e54ad0dba"
+ "jar": "09b4b5e416834e5281dd0dfccac1a10413d6f42c89f133b1c43641e34f33e840",
+ "sources": "ea8192924cc0fd86742be74097c53edbce68232e9b27099ced307912e53ef1ea"
},
- "version": "2.2.4"
+ "version": "2.2.9"
},
"org.apache.sshd:sshd-mina": {
"shasums": {
- "jar": "7399961346882ef50952ae9774e8dd1eac7ce232d2f82c058ade2c20c02591f1",
- "sources": "cc526f8a54d375e87b21419e1c3ce7462637718c6e692fce3940cff98a5151a5"
+ "jar": "da1088d16ba7953444b7d9aefcfc60c9b5e000bf26542d4c3ed0573f692bf14a",
+ "sources": "a8e548e09d8744effe88d83dcd55faa87dc14c37f8a0a595b4c6af3b20e558aa"
},
- "version": "2.18.0"
+ "version": "2.19.0"
},
"org.apache.sshd:sshd-osgi": {
"shasums": {
- "jar": "e771c4239b1c4818c102368d57d0cc082c224eefb76224524473bf7021d1a32f",
- "sources": "dd5838716293c8aeaf9487c50c6b313dff304e7ddca84ce16923f73865ba4d1d"
+ "jar": "0b6b9371c6a97430390b84c23c58f4470927a747e74704a0ca7d19a09a66fe37",
+ "sources": "d11b69618dba155f075571cbd9b21741953c27dfa9a1af09b4024f2183226abe"
},
- "version": "2.18.0"
+ "version": "2.19.0"
},
"org.apache.sshd:sshd-sftp": {
"shasums": {
- "jar": "bf52fd81d63337cb40050c70ee460c80e78a0a4b77f15da7125eb1841f527e93",
- "sources": "5171d9bac4ee76a013beccc1fdeab93fb43aeb84172d19b929ee158b9281c648"
+ "jar": "a3ad30d2feba9fad589c7ad9117fcd9d332823697d59eb424fe2902f4d805575",
+ "sources": "e26b7df972cead5c529bca6ab3e782c27dd43b323f9a54f98d9e23d4f5d03c30"
},
- "version": "2.18.0"
+ "version": "2.19.0"
},
"org.asciidoctor:asciidoctorj": {
"shasums": {
@@ -1284,26 +1297,33 @@
},
"version": "0.24.0"
},
+ "org.eclipse.jetty.ee11:jetty-ee11-servlet": {
+ "shasums": {
+ "jar": "f88e19a29f4e46322df60fe43d5ac4d852edefad923e8c38ef17ff14dc522e84",
+ "sources": "345f50f1a2937968934f354134cf4dd7e2b94bbfec36a177bb015ba675bd345c"
+ },
+ "version": "12.1.11"
+ },
"org.eclipse.jetty.ee8:jetty-ee8-nested": {
"shasums": {
- "jar": "240df6fb5bb28545183cbac87f191c0d21074b17a2c9d4562581a68a8d79c347",
- "sources": "45b7a070430ced248fab480132c2a02c98b64181b619f11fdc27291fc60cdefc"
+ "jar": "b6b4db33b8894688a08f23aba9eee1bc951c66be12a43095f2811121a54018d8",
+ "sources": "2ba3403fb5daf6eaa10e4c3e59fa859e35e4451593d0afd681c2d57b8f6056e9"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty.ee8:jetty-ee8-security": {
"shasums": {
- "jar": "85351106e71c1036256488bbd2c84879d6316c67cdce3b44dda7835daf82e3c9",
- "sources": "d5e6227dfbde0e6beb3cf2976cc64049a85b828143bb328f3de3ef06e5a4803f"
+ "jar": "9f165768c7f7b7a02c7631b38d898f7c16a729b78307c5c3d5940f5de7ee955d",
+ "sources": "7269d79523195225aab585f0bd799914b4d6a9d4746fd71758bb509e6c9b356e"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty.ee8:jetty-ee8-servlet": {
"shasums": {
- "jar": "f593838935e9c15b9cd6bbe8707af6f5e8204f2732c88c84b654962bac660dbe",
- "sources": "53b2477db964532be722a1ff8f53ab4d7f88b87012f4e2b4e3360c5ecd08aff0"
+ "jar": "82bd9eed66b147f243231a164c1157d3dd72cf2d05ae9029e8b12aff9df25fec",
+ "sources": "520e666d8dc4646ccd4b274946278322a393742f54f29d9911be8f46d087c54e"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty.toolchain:jetty-servlet-api": {
"shasums": {
@@ -1314,59 +1334,59 @@
},
"org.eclipse.jetty:jetty-http": {
"shasums": {
- "jar": "090f276739fd9bf8c30511007caec669ea3804b1df1061c37f44467474bee71d",
- "sources": "4d416e2686881085327d3a28e349029d4ae9fc9b44db831291633f18bbf01625"
+ "jar": "0e204f4c71c5d7e138949d50bb7fb15766840ecea6ca24513fb92ee34b363f55",
+ "sources": "1410d5686da1b63ad5d290cdd09b27bd4dfb1a563619a12ef02f63ad8c174eea"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty:jetty-io": {
"shasums": {
- "jar": "448fc0f8f6f5f7251fc46de8e3aae7da14bd7c571a8043dac3dc381d08228fa0",
- "sources": "bf40abffd40e759b16b642e12dfacb7bca1d0c937f5456b62506cc78169f6b56"
+ "jar": "0534454eb445263a8577200f7bd009153d36f3a81ef1543e79ab551846ee3b94",
+ "sources": "d7298bbee4ebde8b35a6566212c9b04c9b19ac9dff7b84eb9a9109ddde805fe9"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty:jetty-jmx": {
"shasums": {
- "jar": "baaaf76b139335125bfa51109c80d271de73f40ee8b5270674f56e90a66e9d81",
- "sources": "b45a4c63d7b7ea44ca6eb9b9c5fbf1d100069bec66942c83ee553d05335a1fcb"
+ "jar": "1207fefdedd9f690d3ac37f1408af6e41f4d9d60a00491e71b3b7137ef95da6f",
+ "sources": "29621d7f6da9909f9214434fef0644c1b5ff20c7d34bc4084daf3de6f1700e86"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty:jetty-security": {
"shasums": {
- "jar": "2f34b7895cec4e3547a1b52e12e7e92b3f3a110bf3c17637f8743ca3f4e42f0c",
- "sources": "9e55b7f04431d4cc723a85a18ddc910e71f5b1a284c28b059d297d07509b4ff8"
+ "jar": "6d21a7cbfd1915735cab6638011998f881161fdeff94936ee8f71e37d3528e79",
+ "sources": "bd00bca2c71ba594de92e10215c705493cbb0fc79149069c6db643911715c9b9"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty:jetty-server": {
"shasums": {
- "jar": "4b0108e87abada7027123deca17186249413232dad0a2bd58a4e70a987b5354a",
- "sources": "5e7693e35285d286dcd0bfdc73d74e6824027eeaaa0a9b4c70c0080475c22f71"
+ "jar": "772f84baf043bbc34edd99de728f3c0b2de642c400dde0fefa763e40de2ea583",
+ "sources": "433739222a745d1a4e14c71b78601065caac517bf307b44f5f85b1cd83da48ae"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty:jetty-session": {
"shasums": {
- "jar": "164649123d15a3f2be5c196e82aa652dff05c75362db71b0f4ab6bb35165020a",
- "sources": "b11e8cfb2db04ac2d1633bccfd47ba9e1b3afca0b75d4354a38140ba3e94c83e"
+ "jar": "d00db0879d2dde6bc8742c3050e995314c5d423e20ca63601cf91add2547a381",
+ "sources": "b86814b8efcff1b2a79bc3ebdd9e02851d131692f2b0e7ab7b9735bbfa86d5bf"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty:jetty-util": {
"shasums": {
- "jar": "c52b4ff62cdacca8a399c611231129e6bd1074db7e3892e78cd106725cdd0ef1",
- "sources": "209630667d8e042f187bed4e754c3f8af7c5247888fc83b8dc97b5bb2134d435"
+ "jar": "8a8d7e063cf312b044927f817b09ad7a4517162d747f7ff891a414e8ef476c26",
+ "sources": "a1cf0445b69a53200633bc8b264cfd88a2927ce4c041048649666ad7cddaaacf"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.eclipse.jetty:jetty-util-ajax": {
"shasums": {
- "jar": "73dfbf388b46c9e2afbc78823ea556f8d25cf851902f99873d03df90becc2b97",
- "sources": "4617309092393ed2f6ed76d915f24b518ccbff8c8731124bf109e346607e86b3"
+ "jar": "2fc1756acdde0bdbc44c693f2c9f5801d7843ccf823b25bd65e6fc52ed77d4f5",
+ "sources": "501f94d01324e7455f365e471acf3c5a3ad6c7c11ceeacf9514f3bc60cdc59e7"
},
- "version": "12.1.10"
+ "version": "12.1.11"
},
"org.hamcrest:hamcrest": {
"shasums": {
@@ -1562,8 +1582,7 @@
"com.google.auto.value:auto-value-annotations",
"com.google.auto:auto-common",
"com.google.guava:guava",
- "com.squareup:javapoet",
- "javax.inject:javax.inject"
+ "com.squareup:javapoet"
],
"com.google.auto:auto-common": [
"com.google.guava:guava"
@@ -1608,13 +1627,9 @@
"org.jspecify:jspecify"
],
"com.google.inject.extensions:guice-assistedinject": [
- "com.google.errorprone:error_prone_annotations",
- "com.google.inject:guice"
+ "com.google.errorprone:error_prone_annotations"
],
- "com.google.inject.extensions:guice-servlet": [
- "com.google.inject:guice"
- ],
- "com.google.inject:guice": [
+ "com.google.inject:guice:jar:classes": [
"aopalliance:aopalliance",
"com.google.guava:guava",
"jakarta.inject:jakarta.inject-api",
@@ -1633,7 +1648,6 @@
"com.google.flogger:flogger-system-backend",
"com.google.flogger:google-extensions",
"com.google.guava:guava",
- "com.google.inject:guice",
"com.google.protobuf:protobuf-java",
"com.ibm.icu:icu4j",
"javax.inject:javax.inject",
@@ -1795,6 +1809,13 @@
"org.commonmark:commonmark-ext-gfm-tables": [
"org.commonmark:commonmark"
],
+ "org.eclipse.jetty.ee11:jetty-ee11-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",
@@ -1851,7 +1872,6 @@
"org.objenesis:objenesis"
],
"org.openid4java:openid4java": [
- "com.google.inject:guice",
"net.sourceforge.nekohtml:nekohtml",
"org.apache.httpcomponents:httpclient",
"xerces:xercesImpl"
@@ -1985,8 +2005,7 @@
"autovalue.shaded.com.google.j2objc.annotations",
"autovalue.shaded.com.squareup.javapoet",
"autovalue.shaded.net.ltgt.gradle.incap",
- "autovalue.shaded.org.checkerframework.checker.nullness.qual",
- "autovalue.shaded.org.checkerframework.framework.qual",
+ "autovalue.shaded.org.jspecify.annotations",
"autovalue.shaded.org.objectweb.asm",
"com.google.auto.value.extension",
"com.google.auto.value.extension.memoized.processor",
@@ -2098,7 +2117,7 @@
"com.google.inject.extensions:guice-servlet": [
"com.google.inject.servlet"
],
- "com.google.inject:guice": [
+ "com.google.inject:guice:jar:classes": [
"com.google.inject",
"com.google.inject.binder",
"com.google.inject.internal",
@@ -2593,6 +2612,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",
@@ -3750,6 +3775,15 @@
"org.commonmark.ext.gfm.tables",
"org.commonmark.ext.gfm.tables.internal"
],
+ "org.eclipse.jetty.ee11:jetty-ee11-servlet": [
+ "org.eclipse.jetty.ee11.servlet",
+ "org.eclipse.jetty.ee11.servlet.internal",
+ "org.eclipse.jetty.ee11.servlet.jmx",
+ "org.eclipse.jetty.ee11.servlet.listener",
+ "org.eclipse.jetty.ee11.servlet.security",
+ "org.eclipse.jetty.ee11.servlet.security.authentication",
+ "org.eclipse.jetty.ee11.servlet.util"
+ ],
"org.eclipse.jetty.ee8:jetty-ee8-nested": [
"org.eclipse.jetty.ee8.nested",
"org.eclipse.jetty.ee8.nested.jmx"
@@ -4348,7 +4382,7 @@
"com.google.inject.extensions:guice-assistedinject:jar:sources",
"com.google.inject.extensions:guice-servlet",
"com.google.inject.extensions:guice-servlet:jar:sources",
- "com.google.inject:guice",
+ "com.google.inject:guice:jar:classes",
"com.google.inject:guice:jar:sources",
"com.google.j2objc:j2objc-annotations",
"com.google.j2objc:j2objc-annotations:jar:sources",
@@ -4422,6 +4456,8 @@
"io.sweers.autotransient:autotransient: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.inject:javax.inject",
@@ -4512,6 +4548,8 @@
"org.commonmark:commonmark-ext-gfm-tables",
"org.commonmark:commonmark-ext-gfm-tables:jar:sources",
"org.commonmark:commonmark:jar:sources",
+ "org.eclipse.jetty.ee11:jetty-ee11-servlet",
+ "org.eclipse.jetty.ee11:jetty-ee11-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",
@@ -4644,7 +4682,7 @@
"com.google.inject.extensions:guice-assistedinject:jar:sources",
"com.google.inject.extensions:guice-servlet",
"com.google.inject.extensions:guice-servlet:jar:sources",
- "com.google.inject:guice",
+ "com.google.inject:guice:jar:classes",
"com.google.inject:guice:jar:sources",
"com.google.j2objc:j2objc-annotations",
"com.google.j2objc:j2objc-annotations:jar:sources",
@@ -4718,6 +4756,8 @@
"io.sweers.autotransient:autotransient: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.inject:javax.inject",
@@ -4808,6 +4848,8 @@
"org.commonmark:commonmark-ext-gfm-tables",
"org.commonmark:commonmark-ext-gfm-tables:jar:sources",
"org.commonmark:commonmark:jar:sources",
+ "org.eclipse.jetty.ee11:jetty-ee11-servlet",
+ "org.eclipse.jetty.ee11:jetty-ee11-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/gerrit/extensions/restapi/Response.java b/java/com/google/gerrit/extensions/restapi/Response.java
index cc0c134..851ec96 100644
--- a/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/java/com/google/gerrit/extensions/restapi/Response.java
@@ -15,7 +15,6 @@
package com.google.gerrit.extensions.restapi;
import static com.google.common.base.Preconditions.checkState;
-import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import com.google.common.collect.ImmutableMultimap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -23,6 +22,10 @@
/** Special return value to mean specific HTTP status codes in a REST API. */
public abstract class Response<T> {
+ // Intentionally keep this constant local to avoid introducing an HTTP library
+ // dependency into the public gerrit-extension-api.
+ private static final int HTTP_INTERNAL_SERVER_ERROR = 500;
+
@SuppressWarnings({"rawtypes"})
private static final Response NONE = new None();
@@ -74,7 +77,7 @@
/** Arbitrary status code with wrapped result. */
public static <T> Response<T> withStatusCode(int statusCode, T value) {
checkState(
- statusCode < SC_INTERNAL_SERVER_ERROR,
+ statusCode < HTTP_INTERNAL_SERVER_ERROR,
"Status code must be < 500. To return an internal server error REST endpoint"
+ " implementations should throw an exception");
return new Impl<>(statusCode, value);
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index 0142031..18334aa 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -38,6 +38,7 @@
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
"//lib/commons:lang3",
+ "//lib/commons:net",
"//lib/errorprone:annotations",
"//lib/flogger:api",
"//lib/guice",
diff --git a/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index 517d5db..3abe786 100644
--- a/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -66,18 +66,21 @@
private final AccountCache accountCache;
private final Config config;
private final String loginHttpHeader;
+ private final RemoteUserUtil remoteUserUtil;
@Inject
ContainerAuthFilter(
DynamicItem<WebSession> session,
AccountCache accountCache,
AuthConfig authConfig,
- @GerritServerConfig Config config) {
+ @GerritServerConfig Config config,
+ RemoteUserUtil remoteUserUtil) {
this.session = session;
this.accountCache = accountCache;
this.config = config;
loginHttpHeader = firstNonNull(emptyToNull(authConfig.getLoginHttpHeader()), AUTHORIZATION);
+ this.remoteUserUtil = remoteUserUtil;
}
@Override
@@ -98,7 +101,7 @@
}
private boolean verify(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
- String username = RemoteUserUtil.getRemoteUser(req, loginHttpHeader);
+ String username = remoteUserUtil.getRemoteUser(req, loginHttpHeader);
if (username == null) {
if (isLfsOverSshRequest(req)) {
// LFS-over-SSH auth request cannot be authorized by container
diff --git a/java/com/google/gerrit/httpd/RemoteUserUtil.java b/java/com/google/gerrit/httpd/RemoteUserUtil.java
index 9ec10e2..3d3e54b 100644
--- a/java/com/google/gerrit/httpd/RemoteUserUtil.java
+++ b/java/com/google/gerrit/httpd/RemoteUserUtil.java
@@ -18,11 +18,71 @@
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.MoreObjects;
+import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.net.util.SubnetUtils;
+@Singleton
public class RemoteUserUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ /**
+ * Request attribute carrying the TCP-peer address before any X-Forwarded-For rewrite.
+ *
+ * <p>HTTP layers that want their requests evaluated against {@code auth.httpTrustedProxyNetworks}
+ * must set this attribute on each request. If the attribute is unset, {@link
+ * HttpServletRequest#getRemoteAddr()} is used as the peer address.
+ *
+ * <p>Gerrit's default Jetty container wires this automatically (see {@code
+ * JettyServer.ForwardedRequestCustomizer}). Other servlet containers (Tomcat, embedded netty,
+ * etc.) can opt in by installing a {@code Filter} or equivalent that runs <em>before</em> any
+ * X-Forwarded-For rewrite (e.g., Tomcat's {@code RemoteIpValve}) and sets this attribute to
+ * {@code request.getRemoteAddr()}.
+ */
+ public static final String PROXY_REMOTE_ADDRESS_ATTR =
+ "com.google.gerrit.httpd.proxyRemoteAddress";
+
+ private final Set<SubnetUtils.SubnetInfo> trustedProxySubnets;
+ private final Set<String> trustedProxyNetworks;
+
+ @Inject
+ RemoteUserUtil(AuthConfig authConfig) {
+ // The full list of `trustedProxyNetworks` is also kept as Set<String>
+ // for allowing the single-IP matching (networks ending with '/32') fast
+ // lookup whilst the full network matching evaluation is performed
+ // through the trustedProxySubnets loop.
+ trustedProxyNetworks = authConfig.getTrustedProxyNetworks();
+
+ try {
+ trustedProxySubnets =
+ trustedProxyNetworks.stream()
+ // Filter out single IPs because they are not matched by
+ // subnetwork matching but rather direct containment in
+ // trustedProxyNetworks
+ .filter(Predicate.not(RemoteUserUtil::isSingleIp))
+ .map(SubnetUtils::new)
+ .map(SubnetUtils::getInfo)
+ .collect(Collectors.toSet());
+ } catch (IllegalArgumentException e) {
+ throw new ProvisionException("Invalid auth trusted proxy definition: " + e.getMessage(), e);
+ }
+ }
+
+ private static boolean isSingleIp(String network) {
+ return network.endsWith("/32");
+ }
+
/**
* Tries to get username from a request with following strategies:
*
@@ -37,8 +97,10 @@
* @return the extracted username or null.
*/
@Nullable
- public static String getRemoteUser(HttpServletRequest req, String loginHeader) {
- if (AUTHORIZATION.equals(loginHeader)) {
+ public String getRemoteUser(HttpServletRequest req, String loginHeader) {
+ boolean isAuthorizationHeader = AUTHORIZATION.equals(loginHeader);
+
+ if (isAuthorizationHeader) {
String user = emptyToNull(req.getRemoteUser());
if (user != null) {
// The container performed the authentication, and has the user
@@ -46,16 +108,55 @@
// configured to honor HTTP authentication.
return user;
}
-
- // If the container didn't do the authentication we might
- // have done it in the front-end web server. Try to split
- // the identity out of the Authorization header and honor it.
- String auth = req.getHeader(AUTHORIZATION);
- return extractUsername(auth);
}
- // Nonstandard HTTP header. We have been told to trust this
- // header blindly as-is.
- return emptyToNull(req.getHeader(loginHeader));
+
+ if (!isRequestFromTrustedProxyNetworks(req)) {
+ return null;
+ }
+
+ String auth = req.getHeader(loginHeader);
+ return isAuthorizationHeader
+ ?
+ // If the container didn't do the authentication we might
+ // have done it in the front-end web server. Try to split
+ // the identity out of the Authorization header and honor it.
+ extractUsername(auth)
+ :
+ // Nonstandard HTTP header. We have been told to trust this
+ // header blindly as-is.
+ emptyToNull(auth);
+ }
+
+ private boolean isRequestFromTrustedProxyNetworks(HttpServletRequest req) {
+ if (trustedProxyNetworks.isEmpty()) {
+ return true;
+ }
+
+ String remoteAddress = getRemoteAddress(req);
+ if (isIpv6Address(remoteAddress)) {
+ logger.atWarning().atMostEvery(1, TimeUnit.MINUTES).log(
+ "IPv6 remote address: %s - trusted proxy enforcement supports only IPv4, HTTP header"
+ + " rejected",
+ remoteAddress);
+ return false;
+ }
+
+ if (trustedProxyNetworks.contains(remoteAddress + "/32")) {
+ return true;
+ }
+
+ if (trustedProxySubnets.stream().anyMatch(subnet -> subnet.isInRange(remoteAddress))) {
+ return true;
+ }
+
+ logger.atWarning().atMostEvery(1, TimeUnit.MINUTES).log(
+ "Untrusted remote address: %s - authentication via HTTP header rejected", remoteAddress);
+ return false;
+ }
+
+ private static String getRemoteAddress(HttpServletRequest req) {
+ return MoreObjects.firstNonNull(
+ (String) req.getAttribute(PROXY_REMOTE_ADDRESS_ATTR), req.getRemoteAddr());
}
/**
@@ -90,4 +191,8 @@
return null;
}
}
+
+ private static boolean isIpv6Address(String ipAddress) {
+ return ipAddress.contains(":");
+ }
}
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index f0a8b89..710adc4 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -67,12 +67,14 @@
private final String externalIdHeader;
private final boolean userNameToLowerCase;
private final ExternalIdKeyFactory externalIdKeyFactory;
+ private final RemoteUserUtil remoteUserUtil;
@Inject
HttpAuthFilter(
DynamicItem<WebSession> webSession,
AuthConfig authConfig,
- ExternalIdKeyFactory externalIdKeyFactory)
+ ExternalIdKeyFactory externalIdKeyFactory,
+ RemoteUserUtil remoteUserUtil)
throws IOException {
this.sessionProvider = webSession;
this.externalIdKeyFactory = externalIdKeyFactory;
@@ -90,6 +92,7 @@
emailHeader = emptyToNull(authConfig.getHttpEmailHeader());
externalIdHeader = emptyToNull(authConfig.getHttpExternalIdHeader());
userNameToLowerCase = authConfig.isUserNameToLowerCase();
+ this.remoteUserUtil = remoteUserUtil;
}
@Override
@@ -138,7 +141,7 @@
}
String getRemoteUser(HttpServletRequest req) {
- String remoteUser = RemoteUserUtil.getRemoteUser(req, loginHeader);
+ String remoteUser = remoteUserUtil.getRemoteUser(req, loginHeader);
return (userNameToLowerCase && remoteUser != null)
? remoteUser.toLowerCase(Locale.US)
: remoteUser;
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 2f17d72..79205ed 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -325,6 +325,7 @@
List<URL> extapi = new ArrayList<>();
move(jars, "gerrit-extension-api-", extapi);
move(jars, "guice-", extapi);
+ move(jars, "jakarta.inject-api-2.0.1.jar", extapi);
move(jars, "javax.inject-1.jar", extapi);
move(jars, "aopalliance-1.0.jar", extapi);
move(jars, "guice-servlet-", extapi);
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index 7c0f258..0964dae 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -315,7 +315,7 @@
new CacheDisplay(
sw,
StreamSupport.stream(cacheMap.spliterator(), false)
- .map(e -> CacheInfoFactory.create(e.getExportName(), e.get()))
+ .map(e -> CacheInfoFactory.create(e.getExportName(), e.get(), true))
.collect(Collectors.toList()))
.displayCaches();
System.out.print(sw.toString());
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 7cde777..20f5bb3 100644
--- a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -23,6 +23,7 @@
import com.google.common.base.Strings;
import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.httpd.RemoteUserUtil;
import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
@@ -33,6 +34,7 @@
import com.google.inject.servlet.GuiceFilter;
import com.google.inject.servlet.GuiceServletContextListener;
import java.lang.management.ManagementFactory;
+import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
@@ -57,6 +59,7 @@
import org.eclipse.jetty.ee8.servlet.FilterHolder;
import org.eclipse.jetty.ee8.servlet.ServletContextHandler;
import org.eclipse.jetty.ee8.servlet.ServletHolder;
+import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.UriCompliance;
@@ -81,6 +84,38 @@
@Singleton
public class JettyServer {
+
+ private static final ForwardedRequestCustomizer FORWARDED_REQUEST_CUSTOMIZER =
+ new ForwardedRequestCustomizer() {
+ @Override
+ public Request customize(Request request, HttpFields.Mutable responseHeaders) {
+ /*
+ * The default behavior of ForwardedRequestCustomizer is to overwrite the remote address
+ * with the value of the X-Forwarded-For header, if present.
+ * However, it does not "remember" the original remote address and therefore would
+ * prevent any validation against it.
+ *
+ * ForwardedRequestCustomizer's original code fragment:
+ * <code>
+ * if (forwarded.hasFor())
+ * {
+ * int forPort = forwarded._for._port > 0 ? forwarded._for._port : request.getRemotePort();
+ * request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._for._host, forPort));
+ * }
+ * </code>
+ *
+ * What we want to achieve here is to remember what it was the original proxy address before
+ * calling super.customize() and give the possibility to fetch it later down the chain.
+ */
+ request.setAttribute(
+ RemoteUserUtil.PROXY_REMOTE_ADDRESS_ATTR,
+ ((InetSocketAddress) request.getConnectionMetaData().getRemoteSocketAddress())
+ .getAddress()
+ .getHostAddress());
+ return super.customize(request, responseHeaders);
+ }
+ };
+
static class Lifecycle implements LifecycleListener {
private final JettyServer server;
private final Config cfg;
@@ -381,12 +416,12 @@
} else if ("proxy-http".equals(u.getScheme())) {
defaultPort = 8080;
- config.addCustomizer(new ForwardedRequestCustomizer());
+ config.addCustomizer(FORWARDED_REQUEST_CUSTOMIZER);
c = newServerConnector(server, acceptors, config);
} else if ("proxy-https".equals(u.getScheme())) {
defaultPort = 8080;
- config.addCustomizer(new ForwardedRequestCustomizer());
+ config.addCustomizer(FORWARDED_REQUEST_CUSTOMIZER);
// For a proxy that terminates TLS, mark every request as HTTPS
// unconditionally. ForwardedRequestCustomizer alone only sets
// isSecure() when the proxy sends X-Forwarded-Proto=https or
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index edede84..756000b 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -35,6 +35,9 @@
exclude = CONSTANTS_SRC + GERRIT_GLOBAL_MODULE_SRC + TESTING_SRC +
PROLOG_SRC,
),
+ # Pin AutoFactory to the javax inject API. See the "Bump auto-factory
+ # version to 1.1.0" change for why; the follow-up removes this pin.
+ javacopts = ["-Acom.google.auto.factory.InjectApi=javax"],
resource_strip_prefix = "resources",
resources = ["//resources/com/google/gerrit/server"],
visibility = ["//visibility:public"],
diff --git a/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java b/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java
index a05baf5..cfe7857 100644
--- a/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java
+++ b/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java
@@ -23,6 +23,7 @@
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
@@ -31,7 +32,6 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
-import javax.inject.Inject;
/**
* An implementation of {@link ServiceUserClassifier} that will consider a user to be a robot if
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
index 560a1c7..1d11bb9 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
@@ -20,8 +20,8 @@
import com.google.gerrit.common.UsedAt.Project;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.ImplementedBy;
-import javax.inject.Inject;
-import javax.inject.Singleton;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
@Singleton
public class ExternalIdKeyFactory {
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
index aa321e7..cb3e894 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
@@ -28,9 +28,9 @@
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.util.Set;
-import javax.inject.Inject;
-import javax.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
diff --git a/java/com/google/gerrit/server/account/storage/notedb/AccountsNoteDbImpl.java b/java/com/google/gerrit/server/account/storage/notedb/AccountsNoteDbImpl.java
index 02ad518..b0c35be 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountsNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountsNoteDbImpl.java
@@ -39,6 +39,7 @@
import com.google.gerrit.server.config.VersionedDefaultPreferences;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -46,7 +47,6 @@
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
-import javax.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
diff --git a/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java b/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
index 663eddd..1820e70 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
@@ -57,7 +57,9 @@
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.RetryableAction.Action;
import com.google.gerrit.server.update.context.RefUpdateContext;
+import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
@@ -67,8 +69,6 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
-import javax.inject.Inject;
-import javax.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
diff --git a/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java b/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
index 676640d..e1caf2f 100644
--- a/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
+++ b/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
@@ -19,9 +19,9 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.PatchSetApproval.UUID;
import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
+import com.google.inject.Singleton;
import java.time.Instant;
import java.util.Locale;
-import javax.inject.Singleton;
/**
* Implementation of {@link PatchSetApprovalUuidGenerator} that returns predictable {@link UUID}.
diff --git a/java/com/google/gerrit/server/cache/CacheInfoFactory.java b/java/com/google/gerrit/server/cache/CacheInfoFactory.java
index dafa186..48122db 100644
--- a/java/com/google/gerrit/server/cache/CacheInfoFactory.java
+++ b/java/com/google/gerrit/server/cache/CacheInfoFactory.java
@@ -24,11 +24,11 @@
public class CacheInfoFactory {
- public static CacheInfo create(Cache<?, ?> cache) {
- return create(null, cache);
+ public static CacheInfo create(Cache<?, ?> cache, boolean includeDiskStats) {
+ return create(null, cache, includeDiskStats);
}
- public static CacheInfo create(String name, Cache<?, ?> cache) {
+ public static CacheInfo create(String name, Cache<?, ?> cache, boolean includeDiskStats) {
CacheInfo cacheInfo = new CacheInfo();
cacheInfo.name = name;
@@ -44,10 +44,12 @@
if (cache instanceof PersistentCache) {
cacheInfo.type = CacheType.DISK;
- PersistentCache.DiskStats diskStats = ((PersistentCache) cache).diskStats();
- cacheInfo.entries.setDisk(diskStats.size());
- cacheInfo.entries.setSpace(diskStats.space());
- cacheInfo.hitRatio.setDisk(diskStats.hitCount(), diskStats.requestCount());
+ if (includeDiskStats) {
+ PersistentCache.DiskStats diskStats = ((PersistentCache) cache).diskStats();
+ cacheInfo.entries.setDisk(diskStats.size());
+ cacheInfo.entries.setSpace(diskStats.space());
+ cacheInfo.hitRatio.setDisk(diskStats.hitCount(), diskStats.requestCount());
+ }
} else {
cacheInfo.type = CacheType.MEM;
}
diff --git a/java/com/google/gerrit/server/cache/CacheMetrics.java b/java/com/google/gerrit/server/cache/CacheMetrics.java
index 7053df0..e6dd0a7 100644
--- a/java/com/google/gerrit/server/cache/CacheMetrics.java
+++ b/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -59,6 +59,12 @@
Long.class,
new Description("Memory eviction count").setGauge().setUnit("evicted entries"),
F_NAME);
+ CallbackMetric1<String, Long> memReq =
+ metrics.newCallbackMetric(
+ "caches/memory_request_count",
+ Long.class,
+ new Description("Memory request count").setGauge().setUnit("requests"),
+ F_NAME);
CallbackMetric1<String, Long> perDiskEnt =
metrics.newCallbackMetric(
"caches/disk_cached",
@@ -79,9 +85,18 @@
.setGauge()
.setUnit("invalidated entries"),
F_NAME);
+ CallbackMetric1<String, Long> perDiskReq =
+ metrics.newCallbackMetric(
+ "caches/disk_request_count",
+ Long.class,
+ new Description("Disk request count for persistent cache")
+ .setGauge()
+ .setUnit("requests"),
+ F_NAME);
ImmutableSet<CallbackMetric<?>> cacheMetrics =
- ImmutableSet.of(memEnt, memHit, memEvict, perDiskEnt, perDiskHit, perDiskInvalid);
+ ImmutableSet.of(
+ memEnt, memHit, memEvict, memReq, perDiskEnt, perDiskHit, perDiskInvalid, perDiskReq);
metrics.newTrigger(
cacheMetrics,
@@ -93,12 +108,14 @@
memEnt.set(name, c.size());
memHit.set(name, cstats.hitRate() * 100);
memEvict.set(name, cstats.evictionCount());
+ memReq.set(name, cstats.requestCount());
if (c instanceof PersistentCache
&& config.getBoolean("cache", "enableDiskStatMetrics", false)) {
PersistentCache.DiskStats d = ((PersistentCache) c).diskStats();
perDiskEnt.set(name, d.size());
perDiskHit.set(name, hitRatio(d));
perDiskInvalid.set(name, d.invalidatedCount());
+ perDiskReq.set(name, d.requestCount());
}
}
cacheMetrics.forEach(CallbackMetric::prune);
diff --git a/java/com/google/gerrit/server/cache/h2/CacheStoreStartupExecutor.java b/java/com/google/gerrit/server/cache/h2/CacheStoreStartupExecutor.java
new file mode 100644
index 0000000..903173c
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/h2/CacheStoreStartupExecutor.java
@@ -0,0 +1,24 @@
+// 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.gerrit.server.cache.h2;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface CacheStoreStartupExecutor {}
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 691c3c6..000b03d 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -87,6 +87,8 @@
private final boolean pruneOnStartup;
private final Schedule schedule;
private final AtomicBoolean isDiskCacheReadOnly;
+ @Nullable private final ExecutorService startupExecutor;
+ private final boolean preWarmForBloomFilter;
@Inject
H2CacheFactory(
@@ -95,6 +97,7 @@
DynamicMap<Cache<?, ?>> cacheMap,
@Nullable @CacheCleanupExecutor ScheduledExecutorService cleanupExecutor,
@Nullable @CacheStoreExecutor ExecutorService storeExecutor,
+ @Nullable @CacheStoreStartupExecutor ExecutorService startupExecutor,
@Nullable @CacheDir Path cacheDir,
Set<CacheOptions> options,
@Named("DiskCacheReadOnly") AtomicBoolean isDiskCacheReadOnly) {
@@ -102,6 +105,7 @@
h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
h2AutoServer = cfg.getBoolean("cache", null, "h2AutoServer", false);
pruneOnStartup = cfg.getBoolean("cachePruning", null, "pruneOnStartup", true);
+ preWarmForBloomFilter = cfg.getBoolean("cache", null, "preWarmForBloomFilter", true);
caches = new ArrayList<>();
schedule =
ScheduleConfig.createSchedule(cfg, "cachePruning")
@@ -112,13 +116,15 @@
this.cleanup = cleanupExecutor;
this.options = options;
this.isDiskCacheReadOnly = isDiskCacheReadOnly;
+ this.startupExecutor = startupExecutor;
}
@Override
public void start() {
if (executor != null) {
+ ExecutorService se = startupExecutor != null ? startupExecutor : executor;
for (H2CacheImpl<?, ?> cache : caches) {
- executor.execute(cache::start);
+ se.execute(cache::start);
if (cleanup != null) {
if (pruneOnStartup) {
@SuppressWarnings("unused")
@@ -135,6 +141,9 @@
TimeUnit.MILLISECONDS);
}
}
+ if (se != executor) {
+ se.shutdown();
+ }
}
}
@@ -142,6 +151,10 @@
public void stop() {
if (executor != null) {
try {
+ if (startupExecutor != null) {
+ startupExecutor.shutdownNow();
+ }
+
if (cleanup != null) {
cleanup.shutdownNow();
}
@@ -213,14 +226,10 @@
}
private <V, K> SqlStore<K, V> newSqlStore(PersistentCacheDef<K, V> def, long maxSize) {
+ String cacheName = def.name() + "-v" + COMPATIBILITY_VERSION;
StringBuilder url = new StringBuilder();
url.append("jdbc:h2:file:")
- .append(
- cacheDir
- .resolve(def.name() + "-v" + COMPATIBILITY_VERSION)
- .toAbsolutePath()
- .toString()
- .replace(";", "\\;"));
+ .append(cacheDir.resolve(cacheName).toAbsolutePath().toString().replace(";", "\\;"));
if (h2CacheSize >= 0) {
url.append(";CACHE_SIZE=");
// H2 CACHE_SIZE is always given in KB
@@ -258,7 +267,9 @@
refreshAfterWrite,
options.contains(CacheOptions.BUILD_BLOOM_FILTER),
options.contains(CacheOptions.TRACK_LAST_ACCESS),
- isDiskCacheReadOnly);
+ isDiskCacheReadOnly,
+ preWarmForBloomFilter,
+ cacheDir.resolve(cacheName + ".mv.db"));
}
private boolean has(String name, String var) {
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 39afb38..d4a6052 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.cache.h2;
+import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.cache.AbstractLoadingCache;
import com.google.common.cache.Cache;
@@ -37,7 +38,10 @@
import com.google.gerrit.util.concurrent.ConcurrentBloomFilter;
import com.google.inject.TypeLiteral;
import java.io.IOException;
+import java.io.InputStream;
import java.io.InvalidClassException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@@ -208,7 +212,9 @@
}
void start() {
+ logger.atFine().log("Opening disk cache %s...", cacheName);
store.open();
+ logger.atFine().log("Finished opening disk cache %s...", cacheName);
}
void stop() {
@@ -346,6 +352,8 @@
private boolean trackLastAccess;
private final AtomicBoolean isDiskCacheReadOnly;
private volatile boolean ensuredSchemaCreation;
+ private final Path cacheFilePath;
+ private final boolean preWarmForBloomFilter;
SqlStore(
String jdbcUrl,
@@ -359,7 +367,9 @@
@Nullable Duration refreshAfterWrite,
boolean buildBloomFilter,
boolean trackLastAccess,
- AtomicBoolean isDiskCacheReadOnly) {
+ AtomicBoolean isDiskCacheReadOnly,
+ boolean preWarmForBloomFilter,
+ Path cacheFilePath) {
this.url = jdbcUrl;
this.keyType = createKeyType(keyType, keySerializer);
this.valueSerializer = valueSerializer;
@@ -370,6 +380,8 @@
this.buildBloomFilter = buildBloomFilter;
this.trackLastAccess = trackLastAccess;
this.isDiskCacheReadOnly = isDiskCacheReadOnly;
+ this.cacheFilePath = cacheFilePath;
+ this.preWarmForBloomFilter = preWarmForBloomFilter;
int cores = Runtime.getRuntime().availableProcessors();
int keep = Math.min(cores, 16);
@@ -422,6 +434,24 @@
}
}
+ void warmupOsPageCache() {
+ // null check because tests use in-memory h2.
+ if (cacheFilePath == null || !Files.exists(cacheFilePath)) {
+ return;
+ }
+ logger.atFine().log("Warming OS page cache for %s", cacheFilePath.getFileName());
+ Stopwatch sw = Stopwatch.createStarted();
+ byte[] buf = new byte[65536];
+ try (InputStream in = Files.newInputStream(cacheFilePath)) {
+ while (in.read(buf) != -1) {}
+ } catch (IOException e) {
+ logger.atWarning().log(
+ "Failed to warm OS page cache for %s: %s", cacheFilePath.getFileName(), e.getMessage());
+ }
+ logger.atFine().log(
+ "Finished warming OS page cache for %s after %s", cacheFilePath.getFileName(), sw);
+ }
+
void open() {
bloomFilter.initIfNeeded();
}
@@ -459,6 +489,9 @@
}
private void buildBloomFilter() {
+ if (preWarmForBloomFilter) {
+ warmupOsPageCache();
+ }
SqlHandle c = null;
try (TraceTimer ignored = TraceContext.newTimer("Build bloom filter", Metadata.empty())) {
c = acquire();
@@ -748,6 +781,7 @@
}
DiskStats diskStats() {
+ warmupOsPageCache();
long size = 0;
long space = 0;
SqlHandle c = null;
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheModule.java b/java/com/google/gerrit/server/cache/h2/H2CacheModule.java
index baf08e6..0ac8123 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheModule.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheModule.java
@@ -115,6 +115,24 @@
}
@Provides
+ @Singleton
+ @Nullable
+ @CacheStoreStartupExecutor
+ ExecutorService createDiskCacheStoreStartupExecutor(
+ @Nullable @CacheDir Path cacheDir, @GerritServerConfig Config cfg) {
+ if (cacheDir == null) {
+ return null;
+ }
+ int startupThreads = cfg.getInt("cache", null, "startupThreads", 1);
+ if (startupThreads > 1) {
+ return new LoggingContextAwareExecutorService(
+ Executors.newFixedThreadPool(
+ 1, new ThreadFactoryBuilder().setNameFormat("DiskCache-Store-Startup-%d").build()));
+ }
+ return null;
+ }
+
+ @Provides
Set<CacheOptions> getOptions() {
return options;
}
diff --git a/java/com/google/gerrit/server/change/CommentsValidator.java b/java/com/google/gerrit/server/change/CommentsValidator.java
index c02fb83..9b75b5e 100644
--- a/java/com/google/gerrit/server/change/CommentsValidator.java
+++ b/java/com/google/gerrit/server/change/CommentsValidator.java
@@ -33,12 +33,12 @@
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListKey;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import javax.inject.Inject;
-import javax.inject.Singleton;
import org.eclipse.jgit.lib.ObjectId;
@Singleton
diff --git a/java/com/google/gerrit/server/change/ParentDataProvider.java b/java/com/google/gerrit/server/change/ParentDataProvider.java
index f5fe120..16b9f71 100644
--- a/java/com/google/gerrit/server/change/ParentDataProvider.java
+++ b/java/com/google/gerrit/server/change/ParentDataProvider.java
@@ -28,11 +28,8 @@
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.ReachabilityChecker;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@Singleton
@@ -70,19 +67,12 @@
/** Returns true if the parent commit {@code parentCommitId} is merged in the target branch. */
private boolean isMergedInTargetBranch(
Project.NameKey project, Repository repo, ObjectId parentCommitId, String targetBranch) {
- try (RevWalk rw = new RevWalk(repo);
- ObjectReader reader = repo.newObjectReader()) {
+ try (RevWalk rw = new RevWalk(repo)) {
Ref targetBranchRef = repo.exactRef(targetBranch);
- if (targetBranchRef == null) {
- return false;
+ if (targetBranchRef != null) {
+ return rw.isMergedInto(
+ rw.parseCommit(parentCommitId), rw.parseCommit(targetBranchRef.getObjectId()));
}
- RevCommit parent = rw.parseCommit(parentCommitId);
- RevCommit targetBranchCommit = rw.parseCommit(targetBranchRef.getObjectId());
- ReachabilityChecker checker = reader.createReachabilityChecker(rw);
- Optional<RevCommit> unreachable =
- checker.areAllReachable(
- ImmutableList.of(parent), ImmutableList.of(targetBranchCommit).stream());
- return unreachable.isEmpty();
} catch (IOException e) {
logger.atWarning().withCause(e).log(
"Failed to check if parent commit %s (project: %s) is merged into target branch %s",
diff --git a/java/com/google/gerrit/server/config/AuthConfig.java b/java/com/google/gerrit/server/config/AuthConfig.java
index 7886cb5..f43a8b8 100644
--- a/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/java/com/google/gerrit/server/config/AuthConfig.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
import com.google.gerrit.server.account.externalids.ExternalId;
@@ -33,6 +34,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
@@ -41,6 +43,7 @@
public class AuthConfig {
private final AuthType authType;
private final String httpHeader;
+ private final ImmutableSet<String> trustedProxyNetworks;
private final String httpDisplaynameHeader;
private final String httpEmailHeader;
private final String httpExternalIdHeader;
@@ -78,6 +81,8 @@
AuthConfig(@GerritServerConfig Config cfg) throws XsrfException {
authType = toType(cfg);
httpHeader = cfg.getString("auth", null, "httpheader");
+ trustedProxyNetworks =
+ ImmutableSet.copyOf(cfg.getStringList("auth", null, "httpTrustedProxyNetworks"));
httpDisplaynameHeader = cfg.getString("auth", null, "httpdisplaynameheader");
httpEmailHeader = cfg.getString("auth", null, "httpemailheader");
httpExternalIdHeader = cfg.getString("auth", null, "httpexternalidheader");
@@ -384,4 +389,8 @@
public boolean isHttpPasswordFallbackEnabled() {
return httpPasswordFallbackEnabled;
}
+
+ public Set<String> getTrustedProxyNetworks() {
+ return trustedProxyNetworks;
+ }
}
diff --git a/java/com/google/gerrit/server/config/FileBasedAllProjectsConfigProvider.java b/java/com/google/gerrit/server/config/FileBasedAllProjectsConfigProvider.java
index db21e1f..77c5617 100644
--- a/java/com/google/gerrit/server/config/FileBasedAllProjectsConfigProvider.java
+++ b/java/com/google/gerrit/server/config/FileBasedAllProjectsConfigProvider.java
@@ -17,8 +17,8 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.util.Optional;
-import javax.inject.Singleton;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 6380db3..f339d76 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -62,9 +62,14 @@
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.DiffOperations;
import com.google.gerrit.server.patch.DiffOptions;
+import com.google.gerrit.server.patch.DiffSummaryKey;
import com.google.gerrit.server.patch.FilePathAdapter;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.util.AccountTemplateUtil;
import com.google.inject.Inject;
@@ -90,6 +95,7 @@
private final AccountCache accountCache;
private final DynamicItem<UrlFormatter> urlFormatter;
private final DiffOperations diffOperations;
+ private final PatchListCache patchListCache;
private final Emails emails;
private final Provider<PersonIdent> myIdent;
private final ChangeData.Factory changeDataFactory;
@@ -105,6 +111,7 @@
Emails emails,
DynamicItem<UrlFormatter> urlFormatter,
DiffOperations diffOperations,
+ PatchListCache patchListCache,
@GerritPersonIdent Provider<PersonIdent> myIdent,
ChangeData.Factory changeDataFactory,
ApprovalsUtil approvalsUtil,
@@ -116,6 +123,7 @@
this.urlFormatter = urlFormatter;
this.emails = emails;
this.diffOperations = diffOperations;
+ this.patchListCache = patchListCache;
this.myIdent = myIdent;
this.changeDataFactory = changeDataFactory;
this.approvalsUtil = approvalsUtil;
@@ -460,7 +468,6 @@
p.ref = patchSet.refName();
p.uploader = asAccountAttribute(patchSet.uploader(), accountLoader);
p.createdOn = patchSet.createdOn().getEpochSecond();
- PatchSet.Id pId = patchSet.id();
try {
p.parents = new ArrayList<>();
RevCommit c = revWalk.parseCommit(ObjectId.fromString(p.revision));
@@ -478,20 +485,20 @@
p.author = asAccountAttribute(author.getAccount(), accountLoader);
}
- Map<String, FileDiffOutput> modifiedFiles =
- diffOperations.listModifiedFilesAgainstParent(
- changeData.project(), patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
- for (FileDiffOutput fileDiff : modifiedFiles.values()) {
- p.sizeDeletions += fileDiff.deletions();
- p.sizeInsertions += fileDiff.insertions();
- }
+ ChangedLines changedLines =
+ patchListCache
+ .getDiffSummary(
+ DiffSummaryKey.fromPatchListKey(
+ PatchListKey.againstBase(patchSet.commitId(), c.getParentCount())),
+ changeData.project())
+ .getChangedLines();
+ p.sizeDeletions = changedLines.deletions;
+ p.sizeInsertions = changedLines.insertions;
p.kind =
changeKindCache.getChangeKind(
revWalk, repoConfig, attributesNodeProvider, changeData, patchSet);
- } catch (IOException | StorageException e) {
+ } catch (IOException | StorageException | PatchListNotAvailableException e) {
logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.id());
- } catch (DiffNotAvailableException e) {
- logger.atSevere().withCause(e).log("Cannot get size information for %s.", pId);
}
return p;
}
diff --git a/java/com/google/gerrit/server/mail/send/DefaultEmailFactories.java b/java/com/google/gerrit/server/mail/send/DefaultEmailFactories.java
index 079f660..dee7f8a 100644
--- a/java/com/google/gerrit/server/mail/send/DefaultEmailFactories.java
+++ b/java/com/google/gerrit/server/mail/send/DefaultEmailFactories.java
@@ -30,12 +30,12 @@
import com.google.gerrit.server.mail.send.InboundEmailRejectionEmailDecorator.InboundEmailError;
import com.google.gerrit.server.mail.send.OutgoingEmail.EmailDecorator;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import javax.inject.Inject;
-import javax.inject.Singleton;
import org.eclipse.jgit.lib.ObjectId;
/** Default versions of Gerrit email notifications. */
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java b/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java
index 25b2ebd..46bf8cc 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java
@@ -23,13 +23,13 @@
import com.google.gerrit.server.CacheRefreshExecutor;
import com.google.gerrit.server.cache.CacheModule;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.google.template.soy.jbcsrc.api.SoySauce;
import java.time.Duration;
import java.util.concurrent.ExecutionException;
-import javax.inject.Provider;
/**
* Provides support for soy templates
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java b/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java
index 6f48aa0..b7cee28 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java
@@ -27,6 +27,7 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.query.change.ChangeNumberVirtualIdAlgorithm;
+import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
@@ -35,7 +36,6 @@
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
-import javax.inject.Inject;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 2d2ec18..3941975 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -21,8 +21,8 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import javax.inject.Inject;
/** Access control management for a user accessing a single change. */
public class ChangeControl extends AbstractChangeControl {
diff --git a/java/com/google/gerrit/server/permissions/RefVisibilityControl.java b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
index 756e922..665f995 100644
--- a/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
+++ b/java/com/google/gerrit/server/permissions/RefVisibilityControl.java
@@ -29,8 +29,8 @@
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
-import javax.inject.Inject;
-import javax.inject.Singleton;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Constants;
/**
diff --git a/java/com/google/gerrit/server/plugins/AutoRegisterUtil.java b/java/com/google/gerrit/server/plugins/AutoRegisterUtil.java
index d592d17..20a032d 100644
--- a/java/com/google/gerrit/server/plugins/AutoRegisterUtil.java
+++ b/java/com/google/gerrit/server/plugins/AutoRegisterUtil.java
@@ -23,7 +23,7 @@
public static Annotation calculateBindAnnotation(Class<Object> impl) {
Annotation n = impl.getAnnotation(Export.class);
if (n == null) {
- n = impl.getAnnotation(javax.inject.Named.class);
+ n = impl.getAnnotation(jakarta.inject.Named.class);
}
if (n == null) {
n = impl.getAnnotation(com.google.inject.name.Named.class);
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index 957b2e2..d0943c7 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -175,7 +175,9 @@
.parse(expression.expressionString());
PredicateResult predicateResult = changeData.evaluatePredicateTree(predicate);
return SubmitRequirementExpressionResult.create(expression, predicateResult);
- } catch (QueryParseException | SubmitRequirementEvaluationException e) {
+ } catch (QueryParseException
+ | SubmitRequirementEvaluationException
+ | IllegalArgumentException e) {
logger.atWarning().withCause(e).log(
"Failed to evaluate submit requirement expression: %s", expression.expressionString());
return SubmitRequirementExpressionResult.error(expression, e.getMessage());
@@ -262,8 +264,8 @@
return timeoutResult(sr, cd);
} catch (ExecutionException | InterruptedException e) {
- logger.atSevere().log("Error evaluating Submit requirement: %s", sr.name());
- throw new RuntimeException(e);
+ logger.atSevere().withCause(e).log("Error evaluating Submit requirement: %s", sr.name());
+ return errorResult(sr, cd, e);
}
}
}
@@ -288,6 +290,29 @@
.build();
}
+ private SubmitRequirementResult errorResult(SubmitRequirement sr, ChangeData cd, Throwable e) {
+ String msg =
+ (e instanceof ExecutionException && e.getCause() != null)
+ ? e.getCause().getMessage()
+ : e.getMessage();
+ if (msg == null) {
+ msg = e.toString();
+ }
+ SubmitRequirementExpressionResult error =
+ SubmitRequirementExpressionResult.error(sr.submittabilityExpression(), msg);
+
+ return SubmitRequirementResult.builder()
+ .legacy(Optional.of(false))
+ .submitRequirement(sr)
+ .patchSetCommitId(cd.currentPatchSet().commitId())
+ .submittabilityExpressionResult(Optional.of(error))
+ .applicabilityExpressionResult(
+ sr.applicabilityExpression().map(SubmitRequirementExpressionResult::notEvaluated))
+ .overrideExpressionResult(
+ sr.overrideExpression().map(SubmitRequirementExpressionResult::notEvaluated))
+ .build();
+ }
+
/**
* Evaluate and return all {@link SubmitRequirement}s.
*
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 3224c09..a3b1e3e 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -811,12 +811,10 @@
}
if (refStates != null) {
ImmutableSet<RefState> refs = refStates.get(project);
- if (refs != null) {
- String metaRef = RefNames.changeMetaRef(getId());
- for (RefState r : refs) {
- if (r.ref().equals(metaRef)) {
- return Optional.of(r.id());
- }
+ String metaRef = RefNames.changeMetaRef(getId());
+ for (RefState r : refs) {
+ if (r.ref().equals(metaRef)) {
+ return Optional.of(r.id());
}
}
}
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 161fb1e..6c1f35e 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -88,7 +88,6 @@
private final ChangeData.Factory changeDataFactory;
private final ChangeNotes.Factory notesFactory;
private final EditByPredicateProvider editByPredicateProvider;
- private int batchSize = 100;
private final Provider<ChangeQueryBuilder.Arguments> queryBuilderArgsProvider;
@Inject
@@ -179,24 +178,7 @@
if (hashes.size() > indexLimit || !indexes.getSearchIndex().isEnabled()) {
return byCommitsOnBranchNotMergedFromDatabase(repo, branch, hashes);
}
- // Safe batch size to avoid large OR queries in Spanner/Lucene
- if (hashes.size() <= batchSize) {
- return byCommitsOnBranchNotMergedFromIndex(branch, hashes);
- }
- List<Predicate<ChangeData>> predicates = new ArrayList<>();
- for (List<String> part : Iterables.partition(hashes, batchSize)) {
- predicates.add(byCommitsOnBranchNotMergedPredicate(branch, part));
- }
- Set<Change.Id> seen = new HashSet<>();
- List<ChangeData> result = new ArrayList<>();
- for (List<ChangeData> partitionResult : query(predicates)) {
- for (ChangeData cd : partitionResult) {
- if (seen.add(cd.getId())) {
- result.add(cd);
- }
- }
- }
- return result;
+ return byCommitsOnBranchNotMergedFromIndex(branch, hashes);
}
private List<ChangeData> byCommitsOnBranchNotMergedFromDatabase(
@@ -232,16 +214,12 @@
private ImmutableList<ChangeData> byCommitsOnBranchNotMergedFromIndex(
BranchNameKey branch, Collection<String> hashes) {
- return query(byCommitsOnBranchNotMergedPredicate(branch, hashes));
- }
-
- private Predicate<ChangeData> byCommitsOnBranchNotMergedPredicate(
- BranchNameKey branch, Collection<String> hashes) {
- return and(
- ref(branch),
- project(branch.project()),
- not(status(Change.Status.MERGED)),
- or(commits(hashes)));
+ return query(
+ and(
+ ref(branch),
+ project(branch.project()),
+ not(status(Change.Status.MERGED)),
+ or(commits(hashes))));
}
private static List<Predicate<ChangeData>> commits(Collection<String> hashes) {
@@ -367,9 +345,4 @@
}
return result.build();
}
-
- @VisibleForTesting
- public void setBatchSizeForTesting(int batchSize) {
- this.batchSize = batchSize;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 3ce7e38..9f4e6d2 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -30,6 +30,8 @@
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.account.AccountAttributeLoader;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
+import com.google.gerrit.server.cancellation.RequestStateProvider;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.PatchSetAttribute;
@@ -224,6 +226,10 @@
AccountAttributeLoader accountLoader = accountAttributeLoaderFactory.create();
List<ChangeAttribute> changeAttributes = new ArrayList<>();
for (ChangeData d : results.entities()) {
+ if (Thread.currentThread().isInterrupted()) {
+ throw new RequestCancelledException(
+ RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, null);
+ }
changeAttributes.add(
buildChangeAttribute(d, repos, revWalks, accountLoader, attributesNodeProviders));
}
diff --git a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
index 8966ec4..05ac95e 100644
--- a/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/restapi/account/QueryAccounts.java
@@ -30,6 +30,7 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.QueryResult;
+import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
@@ -69,6 +70,9 @@
private final Provider<AccountQueryProcessor> queryProcessorProvider;
private final boolean suggestConfig;
private final int suggestFrom;
+ private final AccountControl.Factory accountControlFactory;
+
+ private final AccountVisibility accountVisibility;
private AccountLoader accountLoader;
private boolean suggest;
@@ -134,11 +138,15 @@
AccountLoader.Factory accountLoaderFactory,
AccountQueryBuilder queryBuilder,
Provider<AccountQueryProcessor> queryProcessorProvider,
- @GerritServerConfig Config cfg) {
+ @GerritServerConfig Config cfg,
+ AccountControl.Factory accountControlFactory,
+ AccountVisibility accountVisibility) {
this.permissionBackend = permissionBackend;
this.accountLoaderFactory = accountLoaderFactory;
this.queryBuilder = queryBuilder;
this.queryProcessorProvider = queryProcessorProvider;
+ this.accountControlFactory = accountControlFactory;
+ this.accountVisibility = accountVisibility;
this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
this.options = EnumSet.noneOf(ListAccountsOption.class);
@@ -167,8 +175,14 @@
return Response.ok(Collections.emptyList());
}
+ AccountControl accountControl = accountControlFactory.get();
+ boolean canSeeDetails =
+ accountVisibility == AccountVisibility.ALL
+ || accountControl.getUser().isIdentifiedUser()
+ || accountControl.canViewAll();
+
Set<FillOptions> fillOptions = EnumSet.of(FillOptions.ID);
- if (options.contains(ListAccountsOption.DETAILS)) {
+ if (options.contains(ListAccountsOption.DETAILS) && canSeeDetails) {
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
}
boolean modifyAccountCapabilityChecked = false;
@@ -179,13 +193,12 @@
fillOptions.add(FillOptions.SECONDARY_EMAILS);
}
if (suggest) {
- fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
- fillOptions.add(FillOptions.EMAIL);
+ if (canSeeDetails) {
+ fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
+ fillOptions.add(FillOptions.EMAIL);
- if (modifyAccountCapabilityChecked) {
- fillOptions.add(FillOptions.SECONDARY_EMAILS);
- } else {
- if (permissionBackend.currentUser().test(GlobalPermission.VIEW_SECONDARY_EMAILS)) {
+ if (modifyAccountCapabilityChecked
+ || permissionBackend.currentUser().test(GlobalPermission.VIEW_SECONDARY_EMAILS)) {
fillOptions.add(FillOptions.SECONDARY_EMAILS);
}
}
@@ -197,6 +210,7 @@
throw new MethodNotAllowedException("query disabled");
}
+ queryProcessor.enforceVisibility(true);
queryProcessor.setUserProvidedLimit(limit != null ? limit : 0, /* applyDefaultLimit */ true);
if (start != null) {
diff --git a/java/com/google/gerrit/server/restapi/change/PatchSetCreator.java b/java/com/google/gerrit/server/restapi/change/PatchSetCreator.java
index 7b7ee13..88ec9c9 100644
--- a/java/com/google/gerrit/server/restapi/change/PatchSetCreator.java
+++ b/java/com/google/gerrit/server/restapi/change/PatchSetCreator.java
@@ -44,14 +44,14 @@
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
-import javax.inject.Inject;
-import javax.inject.Provider;
-import javax.inject.Singleton;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 8ac5d0c..e388e3f 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -41,6 +41,7 @@
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
@@ -83,6 +84,7 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.ReviewerAdded;
import com.google.gerrit.server.git.CommitUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -112,6 +114,10 @@
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
@Singleton
public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
@@ -165,6 +171,8 @@
private final CommentsValidator commentsValidator;
private final CommitUtil commitUtil;
+ private final GitRepositoryManager gitManager;
+
@Inject
PostReview(
RetryHelper retryHelper,
@@ -185,7 +193,8 @@
ReviewerAdded reviewerAdded,
ChangeJson.Factory changeJsonFactory,
CommentsValidator commentsValidator,
- CommitUtil commitUtil) {
+ CommitUtil commitUtil,
+ GitRepositoryManager gitManager) {
this.retryHelper = retryHelper;
this.postReviewOpFactory = postReviewOpFactory;
this.changeResourceFactory = changeResourceFactory;
@@ -205,6 +214,7 @@
this.changeJsonFactory = changeJsonFactory;
this.commentsValidator = commentsValidator;
this.commitUtil = commitUtil;
+ this.gitManager = gitManager;
}
@Override
@@ -339,23 +349,34 @@
}
}
- BatchUpdates.Result batchUpdateResult =
- runBatchUpdate(projectState, revision, input, ts, notify, reviewerResults, ccOrReviewer);
- ChangeData cd =
- batchUpdateResult.getChangeData(revision.getProject(), revision.getChange().getId());
- for (ReviewerModification reviewerResult : reviewerResults) {
- reviewerResult.gatherResults(cd);
- }
+ try (Repository repo = gitManager.openRepository(projectState.getProject().getNameKey())) {
+ BatchUpdates.Result batchUpdateResult =
+ runBatchUpdate(
+ projectState, revision, input, ts, notify, reviewerResults, ccOrReviewer, repo);
+ ChangeData cd =
+ batchUpdateResult.getChangeData(revision.getProject(), revision.getChange().getId());
+ ObjectId oldId = cd.metaRevisionOrThrow();
+ ObjectId newId = repo.exactRef(RefNames.changeMetaRef(cd.change().getId())).getObjectId();
- // Sending emails and events from ReviewersOps was suppressed so we can send a single batch
- // email/event here.
- batchEmailReviewers(revision.getUser(), revision.getChange(), reviewerResults, notify);
- batchReviewerEvents(revision.getUser(), cd, revision.getPatchSet(), reviewerResults, ts);
+ if (!oldId.equals(newId)) {
+ // Re-read change to take into account results of the update.
+ cd.reloadChange();
+ }
- if (input.responseFormatOptions != null) {
- output.changeInfo = changeJsonFactory.create(input.responseFormatOptions).format(cd);
- } else {
- output.changeInfo = changeJsonFactory.noOptions().format(cd);
+ for (ReviewerModification reviewerResult : reviewerResults) {
+ reviewerResult.gatherResults(cd);
+ }
+
+ // Sending emails and events from ReviewersOps was suppressed so we can send a single batch
+ // email/event here.
+ batchEmailReviewers(revision.getUser(), revision.getChange(), reviewerResults, notify);
+ batchReviewerEvents(revision.getUser(), cd, revision.getPatchSet(), reviewerResults, ts);
+
+ if (input.responseFormatOptions != null) {
+ output.changeInfo = changeJsonFactory.create(input.responseFormatOptions).format(cd);
+ } else {
+ output.changeInfo = changeJsonFactory.noOptions().format(cd);
+ }
}
return Response.ok(output);
@@ -368,7 +389,8 @@
Instant ts,
NotifyResolver.Result notify,
List<ReviewerModification> reviewerResults,
- boolean ccOrReviewer)
+ boolean ccOrReviewer,
+ Repository repo)
throws UpdateException, RestApiException {
return retryHelper
.changeUpdate(
@@ -376,8 +398,11 @@
updateFactory -> {
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
try (BatchUpdate bu =
- updateFactory.create(
- revision.getChange().getProject(), revision.getUser(), ts)) {
+ updateFactory.create(
+ revision.getChange().getProject(), revision.getUser(), ts);
+ ObjectInserter oi = repo.newObjectInserter();
+ RevWalk revWalk = new RevWalk(oi.newReader())) {
+ bu.setRepository(repo, revWalk, oi);
bu.setNotify(notify);
// Apply reviewer changes first. Revision emails should be sent to the
diff --git a/java/com/google/gerrit/server/restapi/config/GetCache.java b/java/com/google/gerrit/server/restapi/config/GetCache.java
index 23615fa..0f06128 100644
--- a/java/com/google/gerrit/server/restapi/config/GetCache.java
+++ b/java/com/google/gerrit/server/restapi/config/GetCache.java
@@ -26,6 +26,6 @@
@Override
public Response<CacheInfo> apply(CacheResource rsrc) {
- return Response.ok(CacheInfoFactory.create(rsrc.getName(), rsrc.getCache()));
+ return Response.ok(CacheInfoFactory.create(rsrc.getName(), rsrc.getCache(), true));
}
}
diff --git a/java/com/google/gerrit/server/restapi/config/ListCaches.java b/java/com/google/gerrit/server/restapi/config/ListCaches.java
index 6dc17ce..6411615 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCaches.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCaches.java
@@ -35,6 +35,7 @@
import com.google.inject.Inject;
import java.util.Map;
import java.util.TreeMap;
+import java.util.function.Predicate;
import java.util.stream.Stream;
import org.kohsuke.args4j.Option;
@@ -50,22 +51,34 @@
@Option(name = "--format", usage = "output format")
private OutputFormat format;
+ @Option(
+ name = "--include-diskstats",
+ usage = "if set, disk stat collection is included for persistent caches")
+ private boolean includeDiskStats;
+
public ListCaches setFormat(OutputFormat format) {
this.format = format;
return this;
}
+ public ListCaches setIncludeDiskStats(boolean includeDiskStats) {
+ this.includeDiskStats = includeDiskStats;
+ return this;
+ }
+
@Inject
public ListCaches(DynamicMap<Cache<?, ?>> cacheMap) {
this.cacheMap = cacheMap;
}
- public Map<String, CacheInfo> getCacheInfos() {
+ public Map<String, CacheInfo> getCacheInfos(
+ Predicate<String> nameFilter, boolean includeDiskStats) {
Map<String, CacheInfo> cacheInfos = new TreeMap<>();
for (Extension<Cache<?, ?>> e : cacheMap) {
- cacheInfos.put(
- cacheNameOf(e.getPluginName(), e.getExportName()),
- CacheInfoFactory.create(e.getProvider().get()));
+ String name = cacheNameOf(e.getPluginName(), e.getExportName());
+ if (nameFilter.test(name)) {
+ cacheInfos.put(name, CacheInfoFactory.create(e.getProvider().get(), includeDiskStats));
+ }
}
return cacheInfos;
}
@@ -73,7 +86,7 @@
@Override
public Response<Object> apply(ConfigResource rsrc) {
if (format == null) {
- return Response.ok(getCacheInfos());
+ return Response.ok(getCacheInfos(name -> true, includeDiskStats));
}
Stream<String> cacheNames =
Streams.stream(cacheMap)
diff --git a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
index 11d8b19..abbe50d 100644
--- a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
+++ b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
@@ -77,8 +77,8 @@
if (newBranch.isEmpty()) {
continue;
}
- if (!RefPattern.isRE(newBranch) && !newBranch.startsWith(RefNames.REFS)) {
- newBranch = RefNames.REFS_HEADS + newBranch;
+ if (!RefPattern.isRE(newBranch)) {
+ newBranch = RefNames.fullName(newBranch);
}
try {
RefPattern.validate(newBranch);
diff --git a/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java b/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java
index 4e9d432..177f26d 100644
--- a/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java
+++ b/java/com/google/gerrit/server/restapi/project/PostLabelsReview.java
@@ -27,9 +27,9 @@
import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
import com.google.gerrit.server.update.UpdateException;
import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Map;
-import javax.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
diff --git a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java
index 71080a5..2c7bfce 100644
--- a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java
+++ b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirements.java
@@ -23,9 +23,9 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.SubmitRequirementResource;
+import com.google.inject.Inject;
import com.google.inject.Provider;
-import javax.inject.Inject;
-import javax.inject.Singleton;
+import com.google.inject.Singleton;
@Singleton
public class PostSubmitRequirements
diff --git a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java
index f0a371a..c9485fc 100644
--- a/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java
+++ b/java/com/google/gerrit/server/restapi/project/PostSubmitRequirementsReview.java
@@ -25,8 +25,8 @@
import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
import com.google.gerrit.server.update.UpdateException;
import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.io.IOException;
-import javax.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfigReview.java b/java/com/google/gerrit/server/restapi/project/PutConfigReview.java
index 7e6cc19..eda92bb 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfigReview.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfigReview.java
@@ -23,9 +23,9 @@
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
import com.google.gerrit.server.update.UpdateException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.io.IOException;
-import javax.inject.Inject;
-import javax.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
diff --git a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
index 62c88276..d386f85 100644
--- a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
+++ b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
@@ -49,10 +49,10 @@
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
import java.io.IOException;
-import javax.inject.Inject;
-import javax.inject.Provider;
-import javax.inject.Singleton;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
diff --git a/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java b/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java
index 6be71f8..4cc30ec 100644
--- a/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java
+++ b/java/com/google/gerrit/server/rules/prolog/PrologSubmitRuleUtilImpl.java
@@ -20,8 +20,8 @@
import com.google.gerrit.entities.SubmitTypeRecord;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.PrologSubmitRuleUtil;
+import com.google.inject.Inject;
import com.google.inject.Singleton;
-import javax.inject.Inject;
/** Implementation of {@link PrologSubmitRuleUtil}. */
@Singleton
diff --git a/java/com/google/gerrit/server/update/SuperprojectUpdateSubmissionListener.java b/java/com/google/gerrit/server/update/SuperprojectUpdateSubmissionListener.java
index 813bee9..fad29a7 100644
--- a/java/com/google/gerrit/server/update/SuperprojectUpdateSubmissionListener.java
+++ b/java/com/google/gerrit/server/update/SuperprojectUpdateSubmissionListener.java
@@ -21,12 +21,12 @@
import com.google.gerrit.server.submit.MergeOpRepoManager;
import com.google.gerrit.server.submit.SubmoduleOp;
import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
import com.google.inject.Provides;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-import javax.inject.Inject;
import org.eclipse.jgit.transport.ReceiveCommand;
/** Update superprojects after submission is done */
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 7a958b7..bf7c154 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -45,8 +45,13 @@
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
import org.apache.sshd.common.io.IoAcceptor;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.mina.MinaSession;
@@ -79,6 +84,17 @@
@Option(name = "--show-threads", usage = "show detailed thread counts")
private boolean showThreads;
+ @Option(
+ name = "--include-diskstats",
+ usage = "include disk stat collection for persistent caches")
+ private boolean includeDiskStats;
+
+ @Option(
+ name = "--cache",
+ usage = "show the named cache; may be supplied more than once",
+ metaVar = "NAME")
+ private List<String> caches = new ArrayList<>();
+
@Inject private SshDaemon daemon;
@Inject private ListCaches listCaches;
@Inject private GetSummary getSummary;
@@ -153,14 +169,21 @@
}
private Collection<CacheInfo> getCaches() {
- @SuppressWarnings("unchecked")
- Map<String, CacheInfo> caches =
- (Map<String, CacheInfo>) listCaches.apply(new ConfigResource()).value();
- for (Map.Entry<String, CacheInfo> entry : caches.entrySet()) {
+ Map<String, CacheInfo> selected;
+ if (caches.isEmpty()) {
+ selected = listCaches.getCacheInfos(name -> true, includeDiskStats);
+ } else {
+ Set<String> filter =
+ caches.stream().map(n -> n.toLowerCase(Locale.US)).collect(Collectors.toSet());
+ selected =
+ listCaches.getCacheInfos(
+ n -> filter.contains(n.toLowerCase(Locale.US)), includeDiskStats);
+ }
+ for (Map.Entry<String, CacheInfo> entry : selected.entrySet()) {
CacheInfo cache = entry.getValue();
cache.name = entry.getKey();
}
- return caches.values();
+ return selected.values();
}
private void memSummary(MemSummaryInfo memSummary) {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AbstractAccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AbstractAccountIT.java
index 7b2e2dc..e15741e 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AbstractAccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AbstractAccountIT.java
@@ -104,6 +104,7 @@
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInput.CheckAccountsInput;
+import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.common.AccountDetailInfo;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.AccountStateInfo;
@@ -3473,4 +3474,104 @@
return ImmutableList.copyOf(metadataList);
}
}
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void queryAccountsVisibilitySameGroup() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+
+ // Switch to user context (user and user2 are not in the same group by default)
+ requestScopeOperations.setApiUser(user.id());
+
+ // Querying for user2 should return nothing
+ List<AccountInfo> result = gApi.accounts().query("email:" + user2.email()).get();
+ assertThat(result).isEmpty();
+
+ // Querying for user2 with details should also return nothing (reproduces Bypass 1)
+ result =
+ gApi.accounts()
+ .query("email:" + user2.email())
+ .withOption(ListAccountsOption.DETAILS)
+ .get();
+ assertThat(result).isEmpty();
+
+ // Suggesting for user2 should also return nothing (reproduces Bypass 2)
+ result = gApi.accounts().suggestAccounts(user2.email()).get();
+ assertThat(result).isEmpty();
+
+ // Querying for self should still work
+ result = gApi.accounts().query("email:" + user.email()).get();
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0)._accountId).isEqualTo(user.id().get());
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "NONE")
+ public void queryAccountsVisibilityNone() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+
+ requestScopeOperations.setApiUser(user.id());
+
+ // Querying for user2 should return nothing
+ List<AccountInfo> result = gApi.accounts().query("email:" + user2.email()).get();
+ assertThat(result).isEmpty();
+
+ // Querying for self should still return self
+ result = gApi.accounts().query("email:" + user.email()).get();
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0)._accountId).isEqualTo(user.id().get());
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void queryAccountsAnonymousUserSameGroup() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+
+ // Anonymous user is not in same group as user, so query should return empty list
+ List<AccountInfo> result =
+ gApi.accounts().query("email:" + user.email()).withOption(ListAccountsOption.DETAILS).get();
+ assertThat(result).isEmpty();
+
+ // Suggest should also return empty list
+ result = gApi.accounts().suggestAccounts(user.email()).get();
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "VISIBLE_GROUP")
+ public void queryAccountsAnonymousUserVisibleGroup() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+
+ // Anonymous user cannot see user, so query should return empty list
+ List<AccountInfo> result =
+ gApi.accounts().query("email:" + user.email()).withOption(ListAccountsOption.DETAILS).get();
+ assertThat(result).isEmpty();
+
+ // Suggest should also return empty list
+ result = gApi.accounts().suggestAccounts(user.email()).get();
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "ALL")
+ public void queryAccountsAnonymousUserVisibilityAll() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+
+ // Query with details should return details (name, email)
+ List<AccountInfo> result =
+ gApi.accounts().query("email:" + user.email()).withOption(ListAccountsOption.DETAILS).get();
+ assertThat(result).isNotEmpty();
+ for (AccountInfo info : result) {
+ assertThat(info.name).isEqualTo(user.fullName());
+ assertThat(info.email).isEqualTo(user.email());
+ }
+
+ // Suggest should return details (name, email)
+ result = gApi.accounts().suggestAccounts(user.email()).get();
+ assertThat(result).isNotEmpty();
+ for (AccountInfo info : result) {
+ assertThat(info.name).isEqualTo(user.fullName());
+ assertThat(info.email).isEqualTo(user.email());
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
index 984b32d..a1a6755 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
@@ -26,8 +26,8 @@
import com.google.gerrit.mail.MailMessage;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
import java.time.Instant;
-import javax.inject.Inject;
import org.eclipse.jgit.lib.Repository;
import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index 8fc4c65..44190f8 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -57,6 +57,7 @@
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewResult;
import com.google.gerrit.extensions.api.changes.ReviewerInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.Side;
@@ -69,6 +70,7 @@
import com.google.gerrit.extensions.events.ReviewerDeletedListener;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.validators.CommentForValidation;
import com.google.gerrit.extensions.validators.CommentValidationContext;
import com.google.gerrit.extensions.validators.CommentValidator;
@@ -804,6 +806,30 @@
}
@Test
+ public void changeInfoStatusReflectsSynchronousSubmitByCommentAddedListener() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ CommentAddedListener submittingListener =
+ event -> {
+ try {
+ gApi.changes()
+ .id(event.getChange().project, event.getChange()._number)
+ .current()
+ .submit();
+ } catch (RestApiException e) {
+ throw new RuntimeException(e);
+ }
+ };
+
+ try (Registration registration = extensionRegistry.newRegistration().add(submittingListener)) {
+ ReviewResult reviewResult =
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+
+ assertThat(reviewResult.changeInfo.status).isEqualTo(ChangeStatus.MERGED);
+ }
+ }
+
+ @Test
public void submitRulesAreInvokedOnlyOnce() throws Exception {
PushOneCommit.Result r = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
index a987225..7f2e39e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
@@ -21,6 +21,7 @@
import com.google.common.io.BaseEncoding;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.extensions.common.CacheInfo;
import com.google.gson.reflect.TypeToken;
import java.util.Arrays;
@@ -88,4 +89,36 @@
public void listCaches_BadRequest() throws Exception {
adminRestSession.get("/config/server/caches/?format=NONSENSE").assertBadRequest();
}
+
+ @Test
+ public void listCaches_withoutIncludeDiskStats_memCacheUnaffected() throws Exception {
+ RestResponse r = adminRestSession.get("/config/server/caches/");
+ r.assertOK();
+ Map<String, CacheInfo> result =
+ newGson().fromJson(r.getReader(), new TypeToken<Map<String, CacheInfo>>() {}.getType());
+
+ assertThat(result).containsKey("accounts");
+ CacheInfo accountsCacheInfo = result.get("accounts");
+ assertThat(accountsCacheInfo.type).isEqualTo(CacheInfo.CacheType.MEM);
+ assertThat(accountsCacheInfo.entries.mem).isAtLeast(1L);
+ assertThat(accountsCacheInfo.hitRatio.mem).isAtLeast(0);
+
+ assertThat(accountsCacheInfo.entries.disk).isNull();
+ assertThat(accountsCacheInfo.hitRatio.disk).isNull();
+ }
+
+ @Test
+ @UseLocalDisk
+ public void listCaches_withIncludeDiskStats_diskCacheHasDiskStats() throws Exception {
+ RestResponse r = adminRestSession.get("/config/server/caches/?include-diskstats=true");
+ r.assertOK();
+ Map<String, CacheInfo> result =
+ newGson().fromJson(r.getReader(), new TypeToken<Map<String, CacheInfo>>() {}.getType());
+
+ assertThat(result).containsKey("accounts");
+ CacheInfo accountsInfo = result.get("accounts");
+ assertThat(accountsInfo.type).isEqualTo(CacheInfo.CacheType.DISK);
+ assertThat(accountsInfo.entries.mem).isNotNull();
+ assertThat(accountsInfo.entries.disk).isNotNull();
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
index 0e13732..8cf8206 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
@@ -51,10 +51,10 @@
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
import com.google.inject.Module;
import java.util.Collection;
import java.util.stream.StreamSupport;
-import javax.inject.Inject;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.junit.Before;
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/GroupBackedUserPermissionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/GroupBackedUserPermissionIT.java
index 4ad1caa..f0dd3a7 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/GroupBackedUserPermissionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/GroupBackedUserPermissionIT.java
@@ -42,8 +42,8 @@
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.query.change.GroupBackedUser;
import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
import com.google.inject.Module;
-import javax.inject.Inject;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.junit.Before;
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java
index d10d559..2418172 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java
@@ -26,9 +26,9 @@
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.inject.Inject;
import com.google.inject.name.Named;
import java.util.Optional;
-import javax.inject.Inject;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index 945c0b9..52a380f 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -1014,6 +1014,29 @@
assertThat(srResult.errorMessage().get()).isEqualTo("Invalid content pattern.");
}
+ @Test
+ public void invalidPathRegex_returnsErrorExpressionResult() throws Exception {
+ SubmitRequirementExpression exp = SubmitRequirementExpression.create("path:\"^foo[\"");
+
+ SubmitRequirementExpressionResult srResult = evaluator.evaluateExpression(exp, changeData);
+ assertThat(srResult.status()).isEqualTo(SubmitRequirementExpressionResult.Status.ERROR);
+ assertThat(srResult.errorMessage().get()).contains("unexpected end-of-string");
+ }
+
+ @Test
+ public void invalidPathRegex_returnsErrorRequirementResult() throws Exception {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ null,
+ /* submittabilityExpr= */ "path:\"^foo[\"",
+ /* overrideExpr= */ null);
+
+ SubmitRequirementResult srResult = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(srResult.status()).isEqualTo(SubmitRequirementResult.Status.ERROR);
+ assertThat(srResult.submittabilityExpressionResult().get().errorMessage().get())
+ .contains("unexpected end-of-string");
+ }
+
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/index/DefaultIndexBindingIT.java b/javatests/com/google/gerrit/acceptance/testsuite/index/DefaultIndexBindingIT.java
index f6e5fb3..107d241 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/index/DefaultIndexBindingIT.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/index/DefaultIndexBindingIT.java
@@ -23,7 +23,7 @@
import com.google.gerrit.index.testing.AbstractFakeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.testing.SystemPropertiesTestRule;
-import javax.inject.Inject;
+import com.google.inject.Inject;
import org.junit.ClassRule;
import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/index/FakeIndexBindingIT.java b/javatests/com/google/gerrit/acceptance/testsuite/index/FakeIndexBindingIT.java
index acb2e5a..d7afb69 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/index/FakeIndexBindingIT.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/index/FakeIndexBindingIT.java
@@ -21,7 +21,7 @@
import com.google.gerrit.index.testing.AbstractFakeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.testing.SystemPropertiesTestRule;
-import javax.inject.Inject;
+import com.google.inject.Inject;
import org.junit.ClassRule;
import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/index/LuceneIndexBindingIT.java b/javatests/com/google/gerrit/acceptance/testsuite/index/LuceneIndexBindingIT.java
index 5dd6f01..36ea5d1 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/index/LuceneIndexBindingIT.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/index/LuceneIndexBindingIT.java
@@ -21,7 +21,7 @@
import com.google.gerrit.lucene.LuceneChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.testing.SystemPropertiesTestRule;
-import javax.inject.Inject;
+import com.google.inject.Inject;
import org.junit.ClassRule;
import org.junit.Test;
diff --git a/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java b/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java
index f012ee3..ab464e8 100644
--- a/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/RemoteUserUtilTest.java
@@ -15,11 +15,45 @@
package com.google.gerrit.httpd;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.httpd.RemoteUserUtil.PROXY_REMOTE_ADDRESS_ATTR;
import static com.google.gerrit.httpd.RemoteUserUtil.extractUsername;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.when;
+import com.google.common.base.Suppliers;
+import com.google.common.net.HttpHeaders;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.inject.ProvisionException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Set;
+import java.util.function.Supplier;
+import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+@RunWith(MockitoJUnitRunner.class)
public class RemoteUserUtilTest {
+ private static final String CUSTOM_LOGIN_HEADER = "MY_HEADER";
+ private static final String EXPECTED_USER = "user";
+ private static final String BASIC_AUTHENTICATION_USER_HEADER =
+ "Basic "
+ + Base64.getEncoder()
+ .encodeToString((EXPECTED_USER + ":pass").getBytes(StandardCharsets.UTF_8));
+
+ private Supplier<RemoteUserUtil> remoteUserUtil;
+
+ @Mock AuthConfig authConfigMock;
+
+ @Before
+ public void setup() {
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of());
+ remoteUserUtil = Suppliers.memoize(() -> new RemoteUserUtil(authConfigMock));
+ }
+
@Test
public void testExtractUsername() {
assertThat(extractUsername(null)).isNull();
@@ -27,4 +61,165 @@
assertThat(extractUsername("Basic dXNlcjpwYXNzd29yZA==")).isEqualTo("user");
assertThat(extractUsername("Digest username=\"user\", realm=\"test\"")).isEqualTo("user");
}
+
+ @Test
+ public void testExtractUserFromRequestWithCustomHeaderAllowedByDefault() throws Exception {
+ FakeHttpServletRequest fakeRequest = new FakeHttpServletRequest();
+ fakeRequest.addHeader(CUSTOM_LOGIN_HEADER, EXPECTED_USER);
+ assertThat(remoteUserUtil.get().getRemoteUser(fakeRequest, CUSTOM_LOGIN_HEADER))
+ .isEqualTo(EXPECTED_USER);
+ }
+
+ @Test
+ public void testExtractUserFromRequestWithAuthenticationHeaderAllowedByDefault()
+ throws Exception {
+ FakeHttpServletRequest fakeRequest = new FakeHttpServletRequest();
+ fakeRequest.addHeader(HttpHeaders.AUTHORIZATION, BASIC_AUTHENTICATION_USER_HEADER);
+ assertThat(remoteUserUtil.get().getRemoteUser(fakeRequest, HttpHeaders.AUTHORIZATION))
+ .isEqualTo(EXPECTED_USER);
+ }
+
+ @Test
+ public void testExtractUserFromRequestWithCustomHeaderAllowedUsingProxyExactIPv4Matching()
+ throws Exception {
+ String clientIP = "192.168.1.2";
+ String proxyId = "80.78.1.3";
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of(proxyId + "/32"));
+ FakeHttpServletRequest fakeRequest = newFakeHttpRequest(clientIP, EXPECTED_USER);
+ fakeRequest.setAttribute(PROXY_REMOTE_ADDRESS_ATTR, proxyId);
+ assertThat(remoteUserUtil.get().getRemoteUser(fakeRequest, CUSTOM_LOGIN_HEADER))
+ .isEqualTo(EXPECTED_USER);
+ }
+
+ @Test
+ public void testExtractUserFromRequestWithCustomHeaderAllowedWithExactIPv4Matching()
+ throws Exception {
+ String remoteIp = "192.168.1.2";
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of(remoteIp + "/32"));
+ assertThat(
+ remoteUserUtil
+ .get()
+ .getRemoteUser(newFakeHttpRequest(remoteIp, EXPECTED_USER), CUSTOM_LOGIN_HEADER))
+ .isEqualTo(EXPECTED_USER);
+ }
+
+ @Test
+ public void testExtractUserFromRequestWithAuthenticationHeaderAllowedWithExactIPv4Matching()
+ throws Exception {
+ String remoteIp = "192.168.1.2";
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of(remoteIp + "/32"));
+ assertThat(
+ remoteUserUtil
+ .get()
+ .getRemoteUser(
+ newFakeAuthHttpRequest(remoteIp, BASIC_AUTHENTICATION_USER_HEADER),
+ HttpHeaders.AUTHORIZATION))
+ .isEqualTo(EXPECTED_USER);
+ }
+
+ @Test
+ public void testExtractUserFromRequestWithCustomHeaderAllowedWithExactIPv4InAcceptedRange()
+ throws Exception {
+ when(authConfigMock.getTrustedProxyNetworks())
+ .thenReturn(Set.of("10.16.0.0/16", "192.168.1.0/24", "8.8.8.8/32"));
+ assertThat(
+ remoteUserUtil
+ .get()
+ .getRemoteUser(newFakeHttpRequest("10.16.5.1", EXPECTED_USER), CUSTOM_LOGIN_HEADER))
+ .isEqualTo(EXPECTED_USER);
+ }
+
+ @Test
+ public void
+ testExtractUserFromRequestWithAuthenticationHeaderAllowedWithExactIPv4InAcceptedRange()
+ throws Exception {
+ when(authConfigMock.getTrustedProxyNetworks())
+ .thenReturn(Set.of("10.16.0.0/16", "192.168.1.0/24", "8.8.8.8/32"));
+ assertThat(
+ remoteUserUtil
+ .get()
+ .getRemoteUser(
+ newFakeAuthHttpRequest("10.16.5.1", BASIC_AUTHENTICATION_USER_HEADER),
+ HttpHeaders.AUTHORIZATION))
+ .isEqualTo(EXPECTED_USER);
+ }
+
+ @Test
+ public void testExtractUserFromRequestWithCustomHeaderRejectedWithNonMatchingExactIPv4()
+ throws Exception {
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of("2.2.2.2/32"));
+ assertThat(
+ remoteUserUtil
+ .get()
+ .getRemoteUser(newFakeHttpRequest("1.1.1.1", EXPECTED_USER), CUSTOM_LOGIN_HEADER))
+ .isNull();
+ }
+
+ @Test
+ public void testExtractUserFromRequestWithAuthenticationHeaderRejectedWithNonMatchingExactIPv4()
+ throws Exception {
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of("2.2.2.2/32"));
+ assertThat(
+ remoteUserUtil
+ .get()
+ .getRemoteUser(
+ newFakeAuthHttpRequest("1.1.1.1", BASIC_AUTHENTICATION_USER_HEADER),
+ HttpHeaders.AUTHORIZATION))
+ .isNull();
+ }
+
+ @Test
+ public void testExtractUserFromRequestRejectedWithIPv6() throws Exception {
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of("255.255.255.255/32"));
+ assertThat(
+ remoteUserUtil
+ .get()
+ .getRemoteUser(
+ newFakeHttpRequest("2001:0db8:85a3:0000:0000:8a2e:0370:7334", "user"),
+ CUSTOM_LOGIN_HEADER))
+ .isNull();
+ }
+
+ @Test
+ public void testFailWhenUsingAnInvalidProxyNetworkCIDR() throws Exception {
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of("invalid-network"));
+ assertThrows(ProvisionException.class, () -> remoteUserUtil.get());
+ }
+
+ @Test
+ public void testFailWhenUsingSingleIPAsProxyNetworkCIDR() throws Exception {
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of("192.168.0.1"));
+ assertThrows(ProvisionException.class, () -> remoteUserUtil.get());
+ }
+
+ @Test
+ public void testFailWhenUsingIPv6AsProxyNetworkCIDR() throws Exception {
+ when(authConfigMock.getTrustedProxyNetworks()).thenReturn(Set.of("2000::/3"));
+ assertThrows(ProvisionException.class, () -> remoteUserUtil.get());
+ }
+
+ private static FakeHttpServletRequest newFakeHttpRequest(String remoteIp, String expectedUser) {
+ FakeHttpServletRequest fakeRequest =
+ new FakeHttpServletRequest() {
+ @Override
+ public String getRemoteAddr() {
+ return remoteIp;
+ }
+ };
+ fakeRequest.addHeader(CUSTOM_LOGIN_HEADER, expectedUser);
+ return fakeRequest;
+ }
+
+ private static FakeHttpServletRequest newFakeAuthHttpRequest(
+ String remoteIp, String basicAuthHeader) {
+ FakeHttpServletRequest fakeRequest =
+ new FakeHttpServletRequest() {
+ @Override
+ public String getRemoteAddr() {
+ return remoteIp;
+ }
+ };
+ fakeRequest.addHeader(HttpHeaders.AUTHORIZATION, basicAuthHeader);
+ return fakeRequest;
+ }
}
diff --git a/javatests/com/google/gerrit/httpd/auth/container/HttpAuthFilterTest.java b/javatests/com/google/gerrit/httpd/auth/container/HttpAuthFilterTest.java
index a5f8349..32ae2ca 100644
--- a/javatests/com/google/gerrit/httpd/auth/container/HttpAuthFilterTest.java
+++ b/javatests/com/google/gerrit/httpd/auth/container/HttpAuthFilterTest.java
@@ -18,6 +18,7 @@
import static org.mockito.Mockito.doReturn;
import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.httpd.RemoteUserUtil;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
import com.google.gerrit.server.config.AuthConfig;
@@ -37,13 +38,14 @@
@Mock private DynamicItem<WebSession> webSession;
@Mock private ExternalIdKeyFactory externalIdKeyFactory;
@Mock private AuthConfig authConfig;
+ @Mock private RemoteUserUtil remoteUserUtil;
@Test
public void getRemoteDisplaynameShouldReturnDisplaynameHeaderWhenHeaderIsConfiguredAndSet()
throws IOException {
doReturn(DISPLAYNAME_HEADER).when(authConfig).getHttpDisplaynameHeader();
HttpAuthFilter httpAuthFilter =
- new HttpAuthFilter(webSession, authConfig, externalIdKeyFactory);
+ new HttpAuthFilter(webSession, authConfig, externalIdKeyFactory, remoteUserUtil);
FakeHttpServletRequest req = new FakeHttpServletRequest();
req.addHeader(DISPLAYNAME_HEADER, DISPLAYNAME);
@@ -56,7 +58,7 @@
throws IOException {
doReturn(DISPLAYNAME_HEADER).when(authConfig).getHttpDisplaynameHeader();
HttpAuthFilter httpAuthFilter =
- new HttpAuthFilter(webSession, authConfig, externalIdKeyFactory);
+ new HttpAuthFilter(webSession, authConfig, externalIdKeyFactory, remoteUserUtil);
FakeHttpServletRequest req = new FakeHttpServletRequest();
@@ -68,7 +70,7 @@
throws IOException {
doReturn(DISPLAYNAME_HEADER).when(authConfig).getHttpDisplaynameHeader();
HttpAuthFilter httpAuthFilter =
- new HttpAuthFilter(webSession, authConfig, externalIdKeyFactory);
+ new HttpAuthFilter(webSession, authConfig, externalIdKeyFactory, remoteUserUtil);
FakeHttpServletRequest req = new FakeHttpServletRequest();
req.addHeader(DISPLAYNAME_HEADER, "");
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheFactoryTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheFactoryTest.java
index fbd5890..9f55711 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheFactoryTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheFactoryTest.java
@@ -56,6 +56,7 @@
DynamicMap.emptyMap(),
null,
null,
+ null,
cacheDir,
EnumSet.noneOf(CacheOptions.class),
new AtomicBoolean(false));
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index cf8bdda..b4228b6 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -75,7 +75,9 @@
refreshAfterWrite,
true,
true,
- new AtomicBoolean(false));
+ new AtomicBoolean(false),
+ false,
+ null);
}
@Test
diff --git a/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
index ed179a7..bd32241 100644
--- a/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
@@ -30,11 +30,11 @@
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
+import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import com.google.template.soy.jbcsrc.api.SoySauce;
import java.nio.file.Path;
-import javax.inject.Provider;
import org.eclipse.jgit.lib.Config;
import org.junit.Test;
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 32444f7..0f09496 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -3825,36 +3825,6 @@
testByCommitsOnBranchNotMerged(project, ImmutableSet.of(missing));
}
- @Test
- public void byCommitsOnBranchNotMergedPartitioned() throws Exception {
- Project.NameKey project = Project.nameKey("repo");
- createProject(project);
- int n = 10;
- List<String> shas = new ArrayList<>(n);
- List<Integer> expectedIds = new ArrayList<>(n);
- BranchNameKey dest = null;
- try (TestRepository<Repository> repository =
- new TestRepository<>(repoManager.openRepository(project))) {
- for (int i = 0; i < n; i++) {
- ChangeInserter ins = newChange(repository);
- insert(project, ins);
- if (dest == null) {
- dest = ins.getChange().getDest();
- }
- shas.add(ins.getCommitId().name());
- expectedIds.add(ins.getChange().getId().get());
- }
- }
- try (Repository repository = repoManager.openRepository(project)) {
- InternalChangeQuery query = queryProvider.get();
- query.setBatchSizeForTesting(3); // Force partitioning
- Iterable<ChangeData> cds = query.byCommitsOnBranchNotMerged(repository, dest, shas, 11);
- Iterable<Integer> ids = FluentIterable.from(cds).transform(in -> in.getId().get());
- assertThat(ids).hasSize(n);
- assertThat(ids).containsExactlyElementsIn(expectedIds);
- }
- }
-
private void testByCommitsOnBranchNotMerged(Project.NameKey project, Collection<ObjectId> extra)
throws Exception {
int n = 10;
diff --git a/javatests/com/google/gerrit/testing/BUILD b/javatests/com/google/gerrit/testing/BUILD
index 136938a..ac3998eb 100644
--- a/javatests/com/google/gerrit/testing/BUILD
+++ b/javatests/com/google/gerrit/testing/BUILD
@@ -9,6 +9,7 @@
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:guava",
"//lib:jgit",
+ "//lib/guice",
"//lib/truth",
],
)
diff --git a/javatests/com/google/gerrit/testing/GuiceErrorFormattingTest.java b/javatests/com/google/gerrit/testing/GuiceErrorFormattingTest.java
new file mode 100644
index 0000000..72e9da0
--- /dev/null
+++ b/javatests/com/google/gerrit/testing/GuiceErrorFormattingTest.java
@@ -0,0 +1,60 @@
+// 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.gerrit.testing;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.inject.ConfigurationException;
+import com.google.inject.Guice;
+import org.junit.Test;
+
+/**
+ * Regression test for the Guice bundled-ASM incompatibility with Java 25 class files (<a
+ * href="https://github.com/google/guice/issues/1926">guice#1926</a>).
+ *
+ * <p>When Guice formats the source location for a configuration error it reads the offending class
+ * with ASM. The ASM shaded into the default Guice jar cannot read Java 25 (class major version 69)
+ * bytecode: the read throws, Guice logs a warning, and the source silently degrades to {@code
+ * (Unknown Source)}. Building against the {@code classes} classifier plus an external OW2 ASM makes
+ * the read succeed, so the error names the real {@code File.java:line}.
+ *
+ * <p>The check below therefore asserts on the resolved source (present only when ASM works), not on
+ * the swallowed log line — otherwise it would pass with or without the fix.
+ */
+public class GuiceErrorFormattingTest {
+ @Test
+ public void configurationErrorResolvesSourceLocationOnJava25() {
+ ConfigurationException thrown =
+ assertThrows(
+ ConfigurationException.class,
+ () -> Guice.createInjector().getInstance(MissingBinding.class));
+
+ // Sanity: this is the missing-constructor error we set out to provoke.
+ assertThat(thrown).hasMessageThat().contains("No injectable constructor for type");
+
+ // The guard: source formatting must resolve the class's real file:line via ASM. With the
+ // bundled Guice ASM on Java 25 the read fails and the source degrades to "(Unknown Source)".
+ assertThat(thrown).hasMessageThat().contains("GuiceErrorFormattingTest.java:");
+ assertThat(thrown).hasMessageThat().doesNotContain("Unknown Source");
+ }
+
+ private static class MissingBinding {
+ @SuppressWarnings("UnusedMethod")
+ MissingBinding(String value) {
+ throw new AssertionError(value);
+ }
+ }
+}
diff --git a/lib/BUILD b/lib/BUILD
index d8582c4..5c7efa2 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -81,7 +81,7 @@
name = "jgit-servlet",
data = ["//lib:LICENSE-jgit"],
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"],
)
@@ -211,6 +211,13 @@
)
java_library(
+ name = "jspecify",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = ["//visibility:public"],
+ exports = ["@external_deps//:org_jspecify_jspecify"],
+)
+
+java_library(
name = "blame-cache",
data = ["//lib:LICENSE-Apache2.0"],
visibility = ["//visibility:public"],
diff --git a/lib/guice/BUILD b/lib/guice/BUILD
index 8e4272b..12151cd 100644
--- a/lib/guice/BUILD
+++ b/lib/guice/BUILD
@@ -15,7 +15,10 @@
name = "guice-library",
data = ["//lib:LICENSE-Apache2.0"],
visibility = ["//visibility:public"],
- exports = ["@external_deps//:com_google_inject_guice"],
+ exports = [
+ "//lib/ow2:ow2-asm",
+ "@external_deps//:com_google_inject_guice_classes",
+ ],
runtime_deps = ["aopalliance"],
)
diff --git a/modules/jgit b/modules/jgit
index 516c694..bf0f0ad 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 516c69459c8e9aefc292aa938e76203abef3e8ce
+Subproject commit bf0f0ad1cc2ce422604383272a4a4b8b3947b465
diff --git a/package.json b/package.json
index 97acc45..75a1a9f 100644
--- a/package.json
+++ b/package.json
@@ -66,11 +66,11 @@
"test:single:coverage": "yarn --cwd=polygerrit-ui test:single:coverage",
"safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
"eslint": "npm run safe_bazelisk test polygerrit-ui/app:lint_test",
- "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
- "eslintfix:modified": "git diff --name-only --diff-filter=d | grep -E 'polygerrit-ui/app/.*\\.(js|ts)$' | sed 's|^polygerrit-ui/app/||' | xargs -r npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix",
+ "eslintfix": "eslint -c polygerrit-ui/app/eslint-bazel.config.js polygerrit-ui/app --fix --cache",
+ "eslintfix:modified": "git diff --name-only --diff-filter=d | grep -E 'polygerrit-ui/app/.*\\.(js|ts)$' | xargs -r eslint -c polygerrit-ui/app/eslint-bazel.config.js --fix",
"litlint": "npm run safe_bazelisk run polygerrit-ui/app:lit_analysis",
"litlintforCI": "lit-analyzer --strict --rules.no-unknown-property off --rules.no-unknown-tag-name off --rules.no-incompatible-type-binding off --rules.no-incompatible-property-type off --rules.no-invalid-tag-name off --rules.no-property-visibility-mismatch off --rules.no-unknown-attribute off **/elements/**/*.ts",
- "lint": "eslint -c polygerrit-ui/app/eslint-bazel.config.js polygerrit-ui/app",
+ "lint": "eslint -c polygerrit-ui/app/eslint-bazel.config.js polygerrit-ui/app --cache",
"gjf": "./tools/gjf.sh run"
},
"repository": {
diff --git a/plugins/BUILD b/plugins/BUILD
index b831b55..7af6fd2 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -91,12 +91,12 @@
"//lib/guice:guice",
"//lib/guice:guice-assistedinject",
"//lib/guice:guice-servlet",
- "//lib/guice:javax_inject",
"//lib/httpcomponents:httpclient",
"//lib/httpcomponents:httpcore",
"//lib:jgit-servlet",
"//lib:jgit",
"//lib:jsr305",
+ "//lib:jspecify",
"//lib/log:api",
"//lib/log:log4j",
"//lib/mina:sshd",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 35d2770..0b27d4f 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 35d277049e0a5af38e42684fcd21093f77f72748
+Subproject commit 0b27d4f966d89622b44d2c16b96379f37f08ff64
diff --git a/plugins/delete-project b/plugins/delete-project
index b892353..3603583 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit b8923537908122b610d263b480c2f8624126be26
+Subproject commit 3603583484c45e578aeff66885723dfd171932fa
diff --git a/plugins/gitiles b/plugins/gitiles
index ef5e73c..0e3ccb9 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit ef5e73cbc0182e0cbb71ccd22143bf6b02736fdb
+Subproject commit 0e3ccb926a23b972d9f4472e3ba7d874fac481c8
diff --git a/plugins/package.json b/plugins/package.json
index efd7a90..9e4737b 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -34,6 +34,7 @@
"@gerritcodereview/typescript-api": "3.14.0",
"@material/web": "^2.4.1",
"@open-wc/testing": "^4.0.0",
+ "@types/sinon": "^17.0.4",
"@web/dev-server-esbuild": "^1.0.4",
"@web/test-runner": "^0.20.2",
"lit": "^3.3.1",
diff --git a/plugins/pnpm-lock.yaml b/plugins/pnpm-lock.yaml
index a2aca79..1c99c8c 100644
--- a/plugins/pnpm-lock.yaml
+++ b/plugins/pnpm-lock.yaml
@@ -101,6 +101,9 @@
'@open-wc/testing':
specifier: ^4.0.0
version: 4.0.0
+ '@types/sinon':
+ specifier: ^17.0.4
+ version: 17.0.4
'@web/dev-server-esbuild':
specifier: ^1.0.4
version: 1.0.4
diff --git a/plugins/replication b/plugins/replication
index 681d9ab..07782a0 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 681d9ab03db4d2ac3b7bfc81f64e490ddaceebfb
+Subproject commit 07782a0f6bee9e78546d250abd59dd099849a0a8
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 551be7b..26b3007 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -491,10 +491,10 @@
dependencies:
"@types/chai" "^4.2.12"
-"@gerritcodereview/typescript-api@3.13.0":
- version "3.13.0"
- resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.13.0.tgz#c7f5ac20f8b5b575424508eb1deb260a290ca0f1"
- integrity sha512-1TvqARSNO9SXnnCzJKZVz9SFTr4hx1ZzBR0jyRZM6Xd4lLw3YwCfXozD9E8Pm4pfBWmN9WDkURboEG85p7ICag==
+"@gerritcodereview/typescript-api@3.14.0":
+ version "3.14.0"
+ resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.14.0.tgz#0ba1a06b68593e84e5a7525baf91521fdbcb2920"
+ integrity sha512-GhYzh6h/bHUz2cU1pzHwlfWDeGSkGAlBulfvrUyG4N3tuA/jqUp5LSBDeeJHzXgmOSdNE7Qyravjh3wDd0REyg==
"@hapi/bourne@^3.0.0":
version "3.0.0"
@@ -1170,7 +1170,7 @@
"@types/chai" "*"
"@types/sinon" "*"
-"@types/sinon@*":
+"@types/sinon@*", "@types/sinon@^17.0.4":
version "17.0.4"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.4.tgz#fd9a3e8e07eea1a3f4a6f82a972c899e5778f369"
integrity sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==
diff --git a/polygerrit-ui/app/api/annotation.ts b/polygerrit-ui/app/api/annotation.ts
index 7cf200f..38c8141 100644
--- a/polygerrit-ui/app/api/annotation.ts
+++ b/polygerrit-ui/app/api/annotation.ts
@@ -5,6 +5,7 @@
*/
import {
CoverageRange,
+ DiffLayer,
FileRange,
GrDiff,
TokenHighlightEventDetails,
@@ -39,6 +40,11 @@
highlight?: TokenHighlightEventDetails
) => void;
+/**
+ * Factory function to create a DiffLayer.
+ */
+export type DiffLayerFactory = (details: DiffDetails) => DiffLayer;
+
export declare interface AnnotationPluginApi {
/**
* The specified function will be called when a gr-diff component is built,
@@ -56,8 +62,11 @@
*
* The callback receives details of the diff itself and of the highlighted
* token.
- *
- * TODO: Replace with a more general addDiffLayer() endpoint.
*/
addTokenHoverListener(callback: TokenHoverListener): void;
+
+ /**
+ * Register a factory that creates a DiffLayer for each diff view.
+ */
+ addDiffLayer(factory: DiffLayerFactory): void;
}
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 50570f7..45ca607 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -385,11 +385,6 @@
path?: string;
}
-// TODO: Currently unused and not fired.
-export declare interface RenderProgressEventDetail {
- linesRendered: number;
-}
-
/**
* The detail of the 'copy-info' event dispatched by gr-diff.
*/
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index a683f48..32e77ae 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -192,9 +192,9 @@
/**
* EventDetails to be passed to the reportInteraction method for AI agent
- * interactions.
+ * chat interactions.
*/
-export type AiAgentEventDetails = {
+export type AiAgentChatEventDetails = {
agentId: string;
conversationId: string;
// Each agent response in a conversation is a turn.
@@ -206,3 +206,26 @@
// Unique ID of the saved comment draft promoted from the suggestion.
commentId?: string;
};
+
+/**
+ * EventDetails to be passed to the reportInteraction method for AI agent
+ * check interactions.
+ */
+export type AiAgentCheckEventDetails = {
+ // The name of the check that was run.
+ checkName: string;
+ // The description from the check
+ checkDescription: string;
+ // The raw external id for the run result
+ externalId: string;
+ // Unique ID of the saved comment draft promoted from the suggestion.
+ commentId?: string;
+};
+
+/**
+ * EventDetails to be passed to the reportInteraction method for AI agent
+ * interactions.
+ */
+export type AiAgentEventDetails =
+ | AiAgentChatEventDetails
+ | AiAgentCheckEventDetails;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 5f274bc..b35a0ab 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1213,6 +1213,15 @@
continue;
}
const status = this.getLabelStatus(labelInfo);
+ if (status === LabelStatus.REJECT || status === LabelStatus.IMPOSSIBLE) {
+ return null;
+ }
+ if (
+ label === StandardLabels.PRESUBMIT_VERIFIED ||
+ label.toLowerCase().startsWith('presubmit')
+ ) {
+ continue;
+ }
if (status === LabelStatus.NEED) {
if (result) {
// More than one label is missing, so check if Code Review can be
@@ -1221,11 +1230,6 @@
break;
}
result = label;
- } else if (
- status === LabelStatus.REJECT ||
- status === LabelStatus.IMPOSSIBLE
- ) {
- return null;
}
}
// Allow the user to use quick approve to vote the max score on code review
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index c571675..a8a31c6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -2561,6 +2561,57 @@
);
assert.isNotOk(approveButton);
});
+
+ test('ignore presubmit labels for quick approve', async () => {
+ element.change = {
+ ...createChangeViewChange(),
+ current_revision: 'abc1234' as CommitId,
+ labels: {
+ 'Code-Review': {
+ approved: createAccountWithId(1),
+ all: [{value: 2}],
+ },
+ 'Presubmit-Verified': {
+ values: {'-1': '', ' 0': '', '+1': ''},
+ },
+ },
+ permitted_labels: {
+ 'Presubmit-Verified': ['-1', ' 0', '+1'],
+ },
+ };
+ await element.updateComplete;
+ const approveButton = query(
+ element,
+ "gr-button[data-action-key='review']"
+ );
+ assert.isNotOk(approveButton);
+ });
+
+ test('rejected presubmit label blocks quick approve', async () => {
+ element.change = {
+ ...createChangeViewChange(),
+ current_revision: 'abc1234' as CommitId,
+ labels: {
+ 'Code-Review': {
+ values: {'-2': '', '-1': '', ' 0': '', '+1': '', '+2': ''},
+ },
+ 'Presubmit-Verified': {
+ rejected: createAccountWithId(2),
+ values: {'-1': '', ' 0': '', '+1': ''},
+ },
+ },
+ permitted_labels: {
+ 'Code-Review': ['-2', '-1', ' 0', '+1', '+2'],
+ 'Presubmit-Verified': ['-1', ' 0', '+1'],
+ },
+ };
+ await element.updateComplete;
+ const approveButton = query(
+ element,
+ "gr-button[data-action-key='review']"
+ );
+ assert.isNotOk(approveButton);
+ });
});
test('adds download revision action', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 4439344..d9de5f4e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -579,6 +579,7 @@
.statusOrCategory=${statusOrCategory}
.text=${text}
.links=${links}
+ .isAi=${!!run.isAiPowered}
@click=${handler}
@keydown=${(e: KeyboardEvent) => handleSpaceOrEnter(e, handler)}
></gr-checks-chip>`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_screenshot_test.ts
index 1559a1c..045186e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_screenshot_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_screenshot_test.ts
@@ -52,11 +52,13 @@
createRun({
status: RunStatus.COMPLETED,
checkName: 'info-check',
+ isAiPowered: true,
results: [createCheckResult({category: Category.INFO})],
}),
createRun({
status: RunStatus.COMPLETED,
checkName: 'warning-check',
+ isAiPowered: true,
results: [createCheckResult({category: Category.WARNING})],
}),
createRun({
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index 5a1e855..9744398 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -264,6 +264,92 @@
});
});
+ suite('ai chips', () => {
+ test('detailed chip has isAi set based on run.isAiPowered', async () => {
+ element.runs = [
+ createRun({
+ checkName: 'AI Check',
+ status: RunStatus.COMPLETED,
+ isAiPowered: true,
+ results: [createCheckResult({category: Category.ERROR})],
+ }),
+ createRun({
+ checkName: 'Normal Check',
+ status: RunStatus.COMPLETED,
+ isAiPowered: false,
+ results: [createCheckResult({category: Category.ERROR})],
+ }),
+ ];
+ element.showChecksSummary = true;
+ await element.updateComplete;
+
+ const chips = queryAll<GrChecksChip>(element, 'gr-checks-chip');
+ assert.equal(chips.length, 2);
+ assert.isTrue(chips[0].isAi);
+ assert.equal(chips[0].text, 'AI Check');
+ assert.isFalse(chips[1].isAi);
+ assert.equal(chips[1].text, 'Normal Check');
+ });
+
+ test('collapsed chip does not have isAi=true even if run is AI powered', async () => {
+ element.runs = [
+ createRun({
+ status: RunStatus.COMPLETED,
+ isAiPowered: true,
+ results: [createCheckResult({category: Category.SUCCESS})],
+ }),
+ createRun({
+ status: RunStatus.COMPLETED,
+ isAiPowered: false,
+ results: [createCheckResult({category: Category.SUCCESS})],
+ }),
+ createRun({status: RunStatus.RUNNING}),
+ ];
+ element.showChecksSummary = true;
+ await element.updateComplete;
+
+ const chips = queryAll<GrChecksChip>(element, 'gr-checks-chip');
+ const successChip = [...chips].find(
+ c => c.statusOrCategory === Category.SUCCESS
+ );
+ assert.isDefined(successChip);
+ assert.isFalse(successChip.isAi);
+ assert.equal(successChip.text, '2');
+ });
+
+ test('plus-more chip does not have isAi=true even if overflow run is AI powered', async () => {
+ const runs: CheckRun[] = [];
+ for (let i = 0; i < 8; i++) {
+ runs.push(
+ createRun({
+ checkName: `Error ${i}`,
+ status: RunStatus.COMPLETED,
+ isAiPowered: false,
+ results: [createCheckResult({category: Category.ERROR})],
+ })
+ );
+ }
+ runs.push(
+ createRun({
+ checkName: 'Error AI',
+ status: RunStatus.COMPLETED,
+ isAiPowered: true,
+ results: [createCheckResult({category: Category.ERROR})],
+ })
+ );
+
+ element.runs = runs;
+ element.showChecksSummary = true;
+ await element.updateComplete;
+
+ const chips = queryAll<GrChecksChip>(element, 'gr-checks-chip');
+ assert.equal(chips.length, 8);
+ const plusMoreChip = chips[7];
+ assert.equal(plusMoreChip.text, '+ 2 more');
+ assert.isFalse(plusMoreChip.isAi);
+ });
+ });
+
suite('flows summary', () => {
test('renders', async () => {
flowsModel.setState({
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip.ts
index 1875fa5..eedf452 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip.ts
@@ -28,6 +28,9 @@
@property({type: Array})
links: string[] = [];
+ @property({type: Boolean})
+ isAi = false;
+
private readonly reporting = getAppContext().reportingService;
static override get styles() {
@@ -79,6 +82,10 @@
}
gr-icon {
font-size: var(--line-height-small);
+ --gr-icon-size: var(--line-height-small);
+ }
+ gr-icon.ai-sparkle {
+ margin-left: var(--spacing-xs);
}
.checksChip a gr-icon.launch {
color: var(--link-color);
@@ -172,9 +179,14 @@
// 15 is roughly the number of chars for the chip exceeding its 120px width.
return html`
${this.text.length > 15
- ? html` ${this.renderChip(chipClassFullLength, ariaLabel, icon)}`
+ ? html` ${this.renderChip(
+ chipClassFullLength,
+ ariaLabel,
+ icon,
+ this.isAi
+ )}`
: ''}
- ${this.renderChip(chipClass, ariaLabel, icon)}
+ ${this.renderChip(chipClass, ariaLabel, icon, this.isAi)}
`;
}
@@ -191,12 +203,18 @@
return `${label} for check ${this.text}`;
}
- private renderChip(clazz: string, ariaLabel: string, icon: ChecksIcon) {
+ private renderChip(
+ clazz: string,
+ ariaLabel: string,
+ icon: ChecksIcon,
+ isAi: boolean
+ ) {
return html`
<div class=${clazz} role="link" tabindex="0" aria-label=${ariaLabel}>
<gr-icon icon=${icon.name} ?filled=${!!icon.filled}></gr-icon>
${this.renderLinks()}
<div class="text">${this.text}</div>
+ ${isAi ? html`<gr-icon icon="ai" class="ai-sparkle"></gr-icon>` : ''}
</div>
`;
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip_test.ts
index 6816609..c020980 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-checks-chip_test.ts
@@ -85,4 +85,26 @@
`
);
});
+
+ test('renders AI icon', async () => {
+ element.text = 'AI Check';
+ element.statusOrCategory = Category.ERROR;
+ element.isAi = true;
+ await element.updateComplete;
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div
+ aria-label="error for check AI Check"
+ class="checksChip error font-small"
+ role="link"
+ tabindex="0"
+ >
+ <gr-icon icon="error" filled></gr-icon>
+ <div class="text">AI Check</div>
+ <gr-icon icon="ai" class="ai-sparkle"></gr-icon>
+ </div>
+ `
+ );
+ });
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index caa60af..bad9f1e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -981,7 +981,7 @@
flex: 1;
overflow-x: hidden;
}
- .relatedChanges {
+ .commitAside {
flex: 0 1 auto;
overflow: hidden;
padding: var(--spacing-l) 0;
@@ -1005,6 +1005,16 @@
margin: var(--spacing-l) 0;
padding: 0 var(--spacing-l);
}
+ .commitAside gr-endpoint-decorator[name='change-view-commit-aside'] {
+ display: none;
+ }
+ .commitAside
+ gr-endpoint-decorator[name='change-view-commit-aside']:has(
+ :not(gr-endpoint-param):not([hidden])
+ ) {
+ display: block;
+ margin-bottom: var(--spacing-l);
+ }
.showOnEdit {
display: none;
}
@@ -1055,10 +1065,10 @@
position: relative;
}
@media screen and (max-width: 75em) {
- .relatedChanges {
+ .commitAside {
padding: 0;
}
- .relatedChanges gr-related-changes-list {
+ .commitAside gr-related-changes-list {
padding-top: var(--spacing-l);
}
#commitAndRelated {
@@ -1425,7 +1435,13 @@
</gr-endpoint-param>
</gr-endpoint-decorator>
</div>
- <div class="relatedChanges">
+ <div class="commitAside">
+ <gr-endpoint-decorator name="change-view-commit-aside">
+ <gr-endpoint-param name="change" .value=${this.change}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="revision" .value=${this.revision}>
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
<gr-related-changes-list></gr-related-changes-list>
</div>
<div class="emptySpace"></div>
@@ -1923,7 +1939,7 @@
const fileIndex = this.fileList.files.findIndex(f => f.__path === path);
if (fileIndex !== -1) {
this.fileList.fileCursor.setCursorAtIndex(fileIndex, true);
- const isExpanded = this.fileList.expandedFiles.some(f => f.path === path);
+ const isExpanded = this.fileList.expandedFiles.has(path);
if (!isExpanded) {
this.fileList.toggleFileExpandedByIndex(fileIndex);
await this.fileList.updateComplete;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 26b6358..7e11970 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -355,7 +355,12 @@
</gr-endpoint-param>
</gr-endpoint-decorator>
</div>
- <div class="relatedChanges">
+ <div class="commitAside">
+ <gr-endpoint-decorator name="change-view-commit-aside">
+ <gr-endpoint-param name="change"> </gr-endpoint-param>
+ <gr-endpoint-param name="revision">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
<gr-related-changes-list> </gr-related-changes-list>
</div>
<div class="emptySpace"></div>
@@ -1364,6 +1369,91 @@
});
});
+ suite('change-view-commit-aside visibility', () => {
+ let decorator: HTMLElement;
+
+ setup(async () => {
+ element.change = {...createChangeViewChange(), labels: {}};
+ element.revision = createRevision();
+ await element.updateComplete;
+ decorator = element.shadowRoot!.querySelector(
+ 'gr-endpoint-decorator[name="change-view-commit-aside"]'
+ )!;
+ });
+
+ test('hidden by default', () => {
+ assert.equal(getComputedStyle(decorator).display, 'none');
+ });
+
+ test('hidden if plugin component is hidden', async () => {
+ const promise = mockPromise();
+ window.Gerrit.install(
+ promise.resolve,
+ '0.1',
+ 'http://some/plugins/url.js'
+ );
+ const plugin = (await promise) as PluginApi;
+
+ const dummyTagName = 'dummy-aside-component-hidden';
+ if (!customElements.get(dummyTagName)) {
+ customElements.define(
+ dummyTagName,
+ class extends HTMLElement {
+ connectedCallback() {
+ this.setAttribute('hidden', '');
+ }
+ }
+ );
+ }
+
+ plugin.registerCustomComponent('change-view-commit-aside', dummyTagName);
+
+ await new Promise<void>(resolve => {
+ const observer = new MutationObserver(() => {
+ if (decorator.querySelector(dummyTagName)) {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(decorator, {childList: true});
+ });
+
+ await element.updateComplete;
+ assert.equal(getComputedStyle(decorator).display, 'none');
+ });
+
+ test('visible if plugin component is visible', async () => {
+ const promise = mockPromise();
+ window.Gerrit.install(
+ promise.resolve,
+ '0.1',
+ 'http://some/plugins/url.js'
+ );
+ const plugin = (await promise) as PluginApi;
+
+ const dummyTagName = 'dummy-aside-component-visible';
+ if (!customElements.get(dummyTagName)) {
+ customElements.define(dummyTagName, class extends HTMLElement {});
+ }
+
+ plugin.registerCustomComponent('change-view-commit-aside', dummyTagName);
+
+ await new Promise<void>(resolve => {
+ const observer = new MutationObserver(() => {
+ if (decorator.querySelector(dummyTagName)) {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(decorator, {childList: true});
+ });
+
+ await element.updateComplete;
+ assert.equal(getComputedStyle(decorator).display, 'block');
+ assert.notEqual(getComputedStyle(decorator).marginBottom, '0px');
+ });
+ });
+
test('handleToggleStar called when star is tapped', async () => {
element.change = {
...createChangeViewChange(),
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index 95df2fac..f808b1c 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -168,6 +168,7 @@
.commands=${this.computeDownloadCommands()}
.schemes=${this.schemes}
.selectedScheme=${this.selectedScheme}
+ .disableAutoSelect=${true}
show-keyboard-shortcut-tooltips
@selected-scheme-changed=${(e: BindValueChangeEvent) => {
this.selectedScheme = e.detail.value;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
index c107c20..438e151 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.ts
@@ -122,6 +122,7 @@
</section>
<section class="hidden">
<gr-download-commands
+ disable-auto-select=""
id="downloadCommands"
show-keyboard-shortcut-tooltips=""
>
@@ -234,6 +235,14 @@
await element.updateComplete;
});
+ test('passes disableAutoSelect to gr-download-commands', () => {
+ const commands = queryAndAssert<GrDownloadCommands>(
+ element,
+ '#downloadCommands'
+ );
+ assert.isTrue(commands.disableAutoSelect);
+ });
+
test('focuses on first copy link', async () => {
const focusStub = sinon.stub(
queryAndAssert<GrDownloadCommands>(element, '#downloadCommands'),
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index dc28545..f255cb4 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -269,7 +269,7 @@
// Private but used in tests.
@state()
- expandedFiles: PatchSetFile[] = [];
+ expandedFiles: Set<string> = new Set();
// Private but used in tests.
@state()
@@ -1976,29 +1976,26 @@
}
// private but used in test
- toggleFileExpanded(file: PatchSetFile) {
+ toggleFileExpanded(path: string) {
// Is the path in the list of expanded diffs? If so, remove it, otherwise
// add it to the list.
- const indexInExpanded = this.expandedFiles.findIndex(
- f => f.path === file.path
- );
- if (indexInExpanded === -1) {
+ const newExpandedFiles = new Set(this.expandedFiles);
+ if (!newExpandedFiles.has(path)) {
this.reporting.reportInteraction(Interaction.FILE_LIST_DIFF_EXPANDED);
- this.expandedFiles = this.expandedFiles.concat([file]);
+ newExpandedFiles.add(path);
} else {
this.reporting.reportInteraction(Interaction.FILE_LIST_DIFF_COLLAPSED);
- this.expandedFiles = this.expandedFiles.filter(
- (_val, idx) => idx !== indexInExpanded
- );
+ newExpandedFiles.delete(path);
}
- const indexInAll = this.files.findIndex(f => f.__path === file.path);
+ this.expandedFiles = newExpandedFiles;
+ const indexInAll = this.files.findIndex(f => f.__path === path);
this.shadowRoot!.querySelectorAll(`.${FILE_ROW_CLASS}`)[
indexInAll
].scrollIntoView({block: 'nearest'});
}
toggleFileExpandedByIndex(index: number) {
- this.toggleFileExpanded(this.computePatchSetFile(this.files[index]));
+ this.toggleFileExpanded(this.files[index].__path);
}
// Private but used in tests.
@@ -2007,23 +2004,22 @@
return;
}
// Re-render all expanded diffs sequentially.
- this.renderInOrder(this.expandedFiles, this.diffs);
+ this.renderInOrder([...this.expandedFiles], this.diffs);
}
expandAllDiffs() {
- const newFiles = this.files
- .slice(0, this.numFilesShown)
- // TODO(b/419187980): Refactor expandedFiles to use a Set for efficiency.
- .filter(file => !this.expandedFiles.some(f => f.path === file.__path))
- .map(file => this.computePatchSetFile(file));
+ const newExpandedFiles = new Set(this.expandedFiles);
+ this.files.slice(0, this.numFilesShown).forEach(file => {
+ newExpandedFiles.add(file.__path);
+ });
this.reporting.reportInteraction(Interaction.FILE_LIST_ALL_DIFFS_EXPANDED);
- this.expandedFiles = newFiles.concat(this.expandedFiles);
+ this.expandedFiles = newExpandedFiles;
}
collapseAllDiffs() {
this.reporting.reportInteraction(Interaction.FILE_LIST_ALL_DIFFS_COLLAPSED);
- this.expandedFiles = [];
+ this.expandedFiles = new Set();
}
/**
@@ -2119,8 +2115,8 @@
this.fileActionClick(e, file => this.reviewFile(file.path));
}
- private expandedClick(e: MouseEvent | KeyboardEvent) {
- this.fileActionClick(e, file => this.toggleFileExpanded(file));
+ expandedClick(e: MouseEvent | KeyboardEvent) {
+ this.fileActionClick(e, file => this.toggleFileExpanded(file.path));
}
/**
@@ -2152,7 +2148,7 @@
e.preventDefault();
this.fileCursor.setCursor(fileRow.element);
- this.toggleFileExpanded(file);
+ this.toggleFileExpanded(path);
}
private getFileRowFromEvent(e: Event): FileRow | null {
@@ -2424,7 +2420,7 @@
}
async filesChanged() {
- if (this.expandedFiles.length > 0) this.expandedFiles = [];
+ if (this.expandedFiles.size > 0) this.expandedFiles = new Set();
await this.updateCleanlyMergedPaths();
if (!this.files || this.files.length === 0) return;
await this.updateComplete;
@@ -2490,18 +2486,18 @@
return val ? 'true' : 'false';
}
- private isFileExpanded(path: string | undefined) {
- return this.expandedFiles.some(f => f.path === path);
+ isFileExpanded(path: string | undefined) {
+ return path !== undefined && this.expandedFiles.has(path);
}
- private isFileExpandedStr(path: string | undefined) {
+ isFileExpandedStr(path: string | undefined) {
return this.booleanToString(this.isFileExpanded(path));
}
private computeExpandedFiles(): FilesExpandedState {
- if (this.expandedFiles.length === 0) {
+ if (this.expandedFiles.size === 0) {
return FilesExpandedState.NONE;
- } else if (this.expandedFiles.length === this.files.length) {
+ } else if (this.expandedFiles.size === this.files.length) {
return FilesExpandedState.ALL;
}
return FilesExpandedState.SOME;
@@ -2516,18 +2512,21 @@
* @param newFiles The new files that have been added.
* Private but used in tests.
*/
- async expandedFilesChanged(oldFiles: Array<PatchSetFile>) {
+ async expandedFilesChanged(oldFiles?: Set<string>) {
this.filesExpanded = this.computeExpandedFiles();
- const newFiles = this.expandedFiles.filter(
- file => (oldFiles ?? []).findIndex(f => f.path === file.path) === -1
- );
+ const newPaths: string[] = [];
+ for (const path of this.expandedFiles) {
+ if (!oldFiles || !oldFiles.has(path)) {
+ newPaths.push(path);
+ }
+ }
// Required so that the newly created diff view is included in this.diffs.
await this.updateComplete;
- if (newFiles.length) {
- await this.renderInOrder(newFiles, this.diffs);
+ if (newPaths.length) {
+ await this.renderInOrder(newPaths, this.diffs);
}
this.updateDiffCursor();
this.diffCursor?.reInitAndUpdateStops();
@@ -2542,11 +2541,10 @@
*
* @param initialCount The total number of paths in the pass.
*/
- async renderInOrder(files: PatchSetFile[], diffElements: GrDiffHost[]) {
+ async renderInOrder(paths: string[], diffElements: GrDiffHost[]) {
this.reporting.time(Timing.FILE_EXPAND_ALL);
- for (const file of files) {
- const path = file.path;
+ for (const path of paths) {
const diffElem = this.findDiffByPath(path, diffElements);
if (!diffElem) {
this.reporting.error(
@@ -2558,8 +2556,7 @@
diffElem.prefetchDiff();
}
- await asyncForeach(files, async (file, cancel) => {
- const path = file.path;
+ await asyncForeach(paths, async (path, cancel) => {
this.cancelForEachDiff = cancel;
const diffElem = this.findDiffByPath(path, diffElements);
@@ -2582,7 +2579,7 @@
if (
this.loggedIn &&
!this.diffPrefs.manual_review &&
- files.length === 1
+ paths.length === 1
) {
await this.reviewFile(path, true);
}
@@ -2591,7 +2588,7 @@
this.cancelForEachDiff = undefined;
this.reporting.timeEnd(Timing.FILE_EXPAND_ALL, {
- count: files.length,
+ count: paths.length,
height: this.clientHeight,
});
/*
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index be49bfa..4c6bf52 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -1067,41 +1067,41 @@
element.fileCursor.setCursorAtIndex(0);
await element.updateComplete;
assert.equal(element.diffs.length, 0);
- assert.equal(element.expandedFiles.length, 0);
+ assert.equal(element.expandedFiles.size, 0);
pressKey(element, 'i');
await element.updateComplete;
assert.equal(element.diffs.length, 1);
assert.equal(element.diffs[0].path, paths[0]);
- assert.equal(element.expandedFiles.length, 1);
- assert.equal(element.expandedFiles[0].path, paths[0]);
+ assert.equal(element.expandedFiles.size, 1);
+ assert.isTrue(element.expandedFiles.has(paths[0]));
pressKey(element, 'i');
await element.updateComplete;
assert.equal(element.diffs.length, 0);
- assert.equal(element.expandedFiles.length, 0);
+ assert.equal(element.expandedFiles.size, 0);
element.fileCursor.setCursorAtIndex(1);
pressKey(element, 'i');
await element.updateComplete;
assert.equal(element.diffs.length, 1);
assert.equal(element.diffs[0].path, paths[1]);
- assert.equal(element.expandedFiles.length, 1);
- assert.equal(element.expandedFiles[0].path, paths[1]);
+ assert.equal(element.expandedFiles.size, 1);
+ assert.isTrue(element.expandedFiles.has(paths[1]));
pressKey(element, 'I');
await element.updateComplete;
assert.equal(element.diffs.length, paths.length);
- assert.equal(element.expandedFiles.length, paths.length);
+ assert.equal(element.expandedFiles.size, paths.length);
for (const diff of element.diffs) {
- assert.isTrue(element.expandedFiles.some(f => f.path === diff.path));
+ assert.isTrue(element.expandedFiles.has(diff.path!));
}
// since _expandedFilesChanged is stubbed
element.filesExpanded = FilesExpandedState.ALL;
pressKey(element, 'I');
await element.updateComplete;
assert.equal(element.diffs.length, 0);
- assert.equal(element.expandedFiles.length, 0);
+ assert.equal(element.expandedFiles.size, 0);
});
test('r key sets reviewed flag', async () => {
@@ -1340,10 +1340,7 @@
await element.updateComplete;
assert.equal(showHideCheck!.getAttribute('aria-checked'), 'true');
- assert.notEqual(
- element.expandedFiles.findIndex(f => f.path === 'myfile.txt'),
- -1
- );
+ assert.isTrue(element.expandedFiles.has('myfile.txt'));
});
test('diff mode correctly toggles the diffs', async () => {
@@ -1413,8 +1410,8 @@
queryAndAssert<GrIcon>(element, 'gr-icon').icon,
'expand_more'
);
- assert.equal(element.expandedFiles.length, 0);
- element.toggleFileExpanded({path});
+ assert.equal(element.expandedFiles.size, 0);
+ element.toggleFileExpanded(path);
await element.updateComplete;
// Wait for expandedFilesChanged to finish.
await waitEventLoop();
@@ -1425,8 +1422,8 @@
);
assert.equal(renderSpy.callCount, 1);
- assert.isTrue(element.expandedFiles.some(f => f.path === path));
- element.toggleFileExpanded({path});
+ assert.isTrue(element.expandedFiles.has(path));
+ element.toggleFileExpanded(path);
await element.updateComplete;
// Wait for expandedFilesChanged to finish.
await waitEventLoop();
@@ -1436,7 +1433,7 @@
'expand_more'
);
assert.equal(renderSpy.callCount, 1);
- assert.isFalse(element.expandedFiles.some(f => f.path === path));
+ assert.isFalse(element.expandedFiles.has(path));
});
test('expandAllDiffs and collapseAllDiffs', async () => {
@@ -1459,7 +1456,7 @@
await element.updateComplete;
// Wait for expandedFilesChanged to finish.
await waitEventLoop();
- assert.equal(element.expandedFiles.length, 0);
+ assert.equal(element.expandedFiles.size, 0);
assert.equal(element.filesExpanded, FilesExpandedState.NONE);
});
@@ -1490,7 +1487,7 @@
},
];
sinon.stub(element, 'diffs').get(() => diffs);
- element.expandedFiles = element.expandedFiles.concat([{path}]);
+ element.expandedFiles = new Set([path]);
await element.updateComplete;
await waitEventLoop();
await promise;
@@ -1500,12 +1497,12 @@
element.files = [normalize({}, 'foo.bar'), normalize({}, 'baz.bar')];
await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.NONE);
- element.expandedFiles.push({path: 'baz.bar'});
- element.expandedFilesChanged([{path: 'baz.bar'}]);
+ element.expandedFiles.add('baz.bar');
+ element.expandedFilesChanged(new Set(['baz.bar']));
await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.SOME);
- element.expandedFiles.push({path: 'foo.bar'});
- element.expandedFilesChanged([{path: 'foo.bar'}]);
+ element.expandedFiles.add('foo.bar');
+ element.expandedFilesChanged(new Set(['baz.bar', 'foo.bar']));
await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.ALL);
element.collapseAllDiffs();
@@ -1552,10 +1549,7 @@
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any;
- await element.renderInOrder(
- [{path: 'p2'}, {path: 'p1'}, {path: 'p0'}],
- diffs
- );
+ await element.renderInOrder(['p2', 'p1', 'p0'], diffs);
await element.updateComplete;
assert.isFalse(reviewStub.called);
});
@@ -1574,7 +1568,7 @@
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any;
- await element.renderInOrder([{path: 'p2'}], diffs);
+ await element.renderInOrder(['p2'], diffs);
await element.updateComplete;
assert.equal(reviewStub.callCount, 1);
});
@@ -1610,11 +1604,11 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any;
- await element.renderInOrder([{path: 'p'}], diffs);
+ await element.renderInOrder(['p'], diffs);
await element.updateComplete;
assert.isFalse(reviewStub.called);
delete element.diffPrefs.manual_review;
- await element.renderInOrder([{path: 'p'}], diffs);
+ await element.renderInOrder(['p'], diffs);
await element.updateComplete;
// Wait for renderInOrder to finish
await waitEventLoop();
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index 03c79a8..65fc242 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -22,6 +22,7 @@
VotingRangeInfo,
} from '../../../types/common';
import {GrMessage, MessageAnchorTapDetail} from '../gr-message/gr-message';
+import {isServiceUser} from '../../../utils/account-util';
import {getVotingRange} from '../../../utils/label-util';
import {
FormattedReviewerUpdateInfo,
@@ -225,10 +226,28 @@
* Autogenerated messages are unimportant, if there is a message with the same
* tag and a higher revision number.
*/
+function isReviewerUpdateMessage(message: CombinedMessage): boolean {
+ return (
+ (message as FormattedReviewerUpdateInfo).type === 'REVIEWER_UPDATE' ||
+ message.tag === MessageTag.TAG_REVIEWER_UPDATE
+ );
+}
+
function computeIsImportant(
message: CombinedMessage,
allMessages: CombinedMessage[]
) {
+ const author = message.author;
+ const realAuthor =
+ (message as ChangeMessageInfo).real_author ??
+ (message as FormattedReviewerUpdateInfo).realAuthor;
+ if (
+ (isServiceUser(author) || isServiceUser(realAuthor)) &&
+ isReviewerUpdateMessage(message)
+ ) {
+ return false;
+ }
+
if (!message.tag) return true;
const hasSameTag = (m: CombinedMessage) => m.tag === message.tag;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index 66a1951..8ec1bde 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -7,7 +7,7 @@
import '../../../test/common-test-setup';
import './gr-messages-list';
import {CombinedMessage, GrMessagesList, TEST_ONLY} from './gr-messages-list';
-import {MessageTag} from '../../../constants/constants';
+import {AccountTag, MessageTag} from '../../../constants/constants';
import {
query,
queryAll,
@@ -59,7 +59,7 @@
};
};
-const randomMessage = function (params?: ChangeMessageInfo) {
+const randomMessage = function (params?: Partial<ChangeMessageInfo>) {
params = params || ({} as ChangeMessageInfo);
const author1 = {
_account_id: 1115495 as AccountId,
@@ -488,6 +488,68 @@
assert.isFalse(TEST_ONLY.computeIsImportant(m3, [m1, m2, m3]));
});
+ test('isImportant service user reviewer update vs other messages', () => {
+ const reviewerUpdateFromBot = {
+ ...randomMessage(),
+ author: {
+ _account_id: 123 as AccountId,
+ tags: [AccountTag.SERVICE_USER],
+ },
+ type: 'REVIEWER_UPDATE' as const,
+ tag: MessageTag.TAG_REVIEWER_UPDATE as ReviewInputTag,
+ };
+ const reviewerUpdateWithRealAuthorBot = {
+ ...randomMessage(),
+ author: {
+ _account_id: 456 as AccountId,
+ },
+ real_author: {
+ _account_id: 123 as AccountId,
+ tags: [AccountTag.SERVICE_USER],
+ },
+ type: 'REVIEWER_UPDATE' as const,
+ tag: MessageTag.TAG_REVIEWER_UPDATE as ReviewInputTag,
+ };
+ const formattedReviewerUpdateWithRealAuthorBot = {
+ author: {
+ _account_id: 456 as AccountId,
+ },
+ realAuthor: {
+ _account_id: 123 as AccountId,
+ tags: [AccountTag.SERVICE_USER],
+ },
+ date: '2020-01-01 00:00:00.000000000' as Timestamp,
+ type: 'REVIEWER_UPDATE' as const,
+ tag: MessageTag.TAG_REVIEWER_UPDATE as const,
+ updates: [],
+ };
+ const commentFromBot = randomMessage({
+ author: {
+ _account_id: 123 as AccountId,
+ tags: [AccountTag.SERVICE_USER],
+ },
+ message: 'Build succeeded: 10 tests passed',
+ });
+ assert.isFalse(
+ TEST_ONLY.computeIsImportant(reviewerUpdateFromBot, [
+ reviewerUpdateFromBot,
+ ])
+ );
+ assert.isFalse(
+ TEST_ONLY.computeIsImportant(reviewerUpdateWithRealAuthorBot, [
+ reviewerUpdateWithRealAuthorBot,
+ ])
+ );
+ assert.isFalse(
+ TEST_ONLY.computeIsImportant(formattedReviewerUpdateWithRealAuthorBot, [
+ formattedReviewerUpdateWithRealAuthorBot,
+ ])
+ );
+ assert.isTrue(
+ TEST_ONLY.computeIsImportant(commentFromBot, [commentFromBot])
+ );
+ });
+
test('isImportant is evaluated after tag update', async () => {
const m1 = randomMessage({
...randomMessage(),
diff --git a/polygerrit-ui/app/elements/chat-panel/gemini-message_test.ts b/polygerrit-ui/app/elements/chat-panel/gemini-message_test.ts
index 2c7ae69..5618b06 100644
--- a/polygerrit-ui/app/elements/chat-panel/gemini-message_test.ts
+++ b/polygerrit-ui/app/elements/chat-panel/gemini-message_test.ts
@@ -26,7 +26,7 @@
import {chatProvider, createChange} from '../../test/test-data-generators';
import {ParsedChangeInfo} from '../../types/types';
import {CommentsModel} from '../../models/comments/comments-model';
-import {AiAgentEventDetails, Interaction} from '../../constants/reporting';
+import {AiAgentChatEventDetails, Interaction} from '../../constants/reporting';
import {getAppContext} from '../../services/app-context';
suite('gemini-message tests', () => {
@@ -260,7 +260,7 @@
reportStub.firstCall.args[0],
Interaction.AI_AGENT_SUGGESTIONS_SHOWN
);
- const details = reportStub.firstCall.args[1] as AiAgentEventDetails;
+ const details = reportStub.firstCall.args[1] as AiAgentChatEventDetails;
assert.equal(details.conversationId, 'test-conversation-id');
assert.equal(details.agentId, 'custom-agent-id');
assert.equal(details.commentCount, 1);
@@ -308,7 +308,7 @@
.find(c => c.args[0] === Interaction.AI_AGENT_SUGGESTION_TO_COMMENT);
assert.isOk(call, 'Expected AI_AGENT_SUGGESTION_TO_COMMENT to be reported');
- const details = call.args[1] as AiAgentEventDetails;
+ const details = call.args[1] as AiAgentChatEventDetails;
assert.equal(details.conversationId, 'test-conversation-id');
assert.equal(details.agentId, 'custom-agent-id');
assert.equal(details.commentId, 'test-comment-id');
@@ -352,7 +352,7 @@
'Expected AI_AGENT_SUGGESTION_COPY_BUTTON_CLICKED to be reported'
);
- const details = call.args[1] as AiAgentEventDetails;
+ const details = call.args[1] as AiAgentChatEventDetails;
assert.equal(details.conversationId, 'test-conversation-id');
assert.equal(details.agentId, 'custom-agent-id');
});
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label.ts b/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label.ts
index ddf37ab..74af4d8 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label.ts
@@ -75,10 +75,12 @@
if (runs.length === 1 && runs[0].statusLink) {
links.push(runs[0].statusLink);
}
+ const isAi = runs.some(run => run.isAiPowered);
return html`<gr-checks-chip
.text=${`${runsCount}`}
.links=${links}
.statusOrCategory=${category}
+ .isAi=${isAi}
@click=${() => {
fireShowTab(this, Tab.CHECKS, false, {
checksTab: {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label_test.ts
index cac448a..641f2f8 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-chip-for-label_test.ts
@@ -58,4 +58,22 @@
</div>`
);
});
+
+ test('sets isAi to true when check run is AI powered', async () => {
+ element.runs = [
+ createRun({
+ labelName: 'Verified',
+ isAiPowered: true,
+ results: [
+ createCheckResult({
+ category: Category.ERROR,
+ }),
+ ],
+ }),
+ ];
+ await element.updateComplete;
+
+ const checksChip = queryAndAssert<GrChecksChip>(element, 'gr-checks-chip');
+ assert.isTrue(checksChip.isAi);
+ });
});
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
index c28df76..71085f2 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
@@ -155,6 +155,7 @@
category: 'ERROR',
summary: 'Test Summary',
message: 'Test Message',
+ isAiPowered: true,
externalId: JSON.stringify({
agentId: 'test-agent',
conversationId: 'test-conv',
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index bfdde07..a9284c5 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -29,7 +29,11 @@
import {subscribe} from '../../lit/subscription-controller';
import {assert} from '../../../utils/common-util';
import {resolve} from '../../../models/dependency';
-import {createChangeUrl} from '../../../models/views/change';
+import {
+ changeViewModelToken,
+ createApplyFixUrl,
+} from '../../../models/views/change';
+
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {userModelToken} from '../../../models/user/user-model';
import {modalStyles} from '../../../styles/gr-modal-styles';
@@ -114,6 +118,8 @@
private readonly getNavigation = resolve(this, navigationToken);
+ private readonly getViewModel = resolve(this, changeViewModelToken);
+
private readonly reporting = getAppContext().reportingService;
private readonly syntaxLayer = new GrSyntaxLayerWorker(
@@ -481,16 +487,22 @@
});
}
if (res?.ok) {
+ const currentChildView = this.getViewModel().getState()?.childView;
+ const filePath =
+ fixSuggestion.replacements[0]?.path ??
+ this.currentPreviews[0]?.filepath;
this.getNavigation().setUrl(
- createChangeUrl({
+ createApplyFixUrl({
change,
- patchNum: EDIT,
basePatchNum: patchNum as BasePatchSetNum,
forceReload: !this.hasEdit,
+ filePath,
+ currentChildView,
})
);
this.close(true);
}
+
this.isApplyFixLoading = false;
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index 1d35430..21ce8ed 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -14,12 +14,15 @@
import {GrApplyFixDialog} from './gr-apply-fix-dialog';
import {PatchSetNum, PatchSetNumber} from '../../../types/common';
import {
+ createChangeViewState,
+ createDiffViewState,
createFixSuggestionInfo,
createParsedChange,
createRange,
createRevisions,
getCurrentRevision,
} from '../../../test/test-data-generators';
+import {changeViewModelToken} from '../../../models/views/change';
import {createDefaultDiffPrefs} from '../../../constants/constants';
import {OpenFixPreviewEventDetail} from '../../../types/events';
import {GrButton} from '../../shared/gr-button/gr-button';
@@ -227,4 +230,70 @@
'/c/test-project/+/42/2..edit?forceReload=true'
);
});
+
+ suite('handleApplyFix navigation', () => {
+ setup(() => {
+ stubRestApi('applyFixSuggestion').returns(
+ Promise.resolve(new Response(null, {status: 200}))
+ );
+ });
+
+ test('navigates to createDiffUrl when in Diff View', async () => {
+ testResolver(changeViewModelToken).setState(createDiffViewState());
+ const fixDetail: OpenFixPreviewEventDetail = {
+ patchNum: 2 as PatchSetNum,
+ fixSuggestions: [
+ {
+ ...createFixSuggestionInfo('fix_1'),
+ replacements: [
+ {
+ path: 'file1.txt',
+ replacement: 'new content',
+ range: createRange(),
+ },
+ ],
+ },
+ ],
+ onCloseFixPreviewCallbacks: [],
+ };
+ await open(fixDetail);
+
+ await element.handleApplyFix(new CustomEvent('confirm'));
+
+ assert.isTrue(setUrlStub.calledOnce);
+ assert.equal(
+ setUrlStub.lastCall.firstArg,
+ '/c/test-project/+/42/2..edit/file1.txt?forceReload=true'
+ );
+ });
+
+ test('navigates to createChangeUrl when in Change View', async () => {
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+ const fixDetail: OpenFixPreviewEventDetail = {
+ patchNum: 2 as PatchSetNum,
+ fixSuggestions: [
+ {
+ ...createFixSuggestionInfo('fix_1'),
+ replacements: [
+ {
+ path: 'file1.txt',
+ replacement: 'new content',
+ range: createRange(),
+ },
+ ],
+ },
+ ],
+ onCloseFixPreviewCallbacks: [],
+ };
+ await open(fixDetail);
+
+ await element.handleApplyFix(new CustomEvent('confirm'));
+
+ assert.isTrue(setUrlStub.calledOnce);
+ assert.equal(
+ setUrlStub.lastCall.firstArg,
+ '/c/test-project/+/42/2..edit?forceReload=true'
+ );
+ });
+ });
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 907c764..0a6538b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -35,6 +35,7 @@
PARENT,
PatchRange,
PatchSetNum,
+ PreferencesInfo,
RepoName,
RevisionPatchSetNum,
} from '../../../types/common';
@@ -71,6 +72,7 @@
RangeSelectedEventDetail,
RenderPreferences,
} from '../../../api/diff';
+import {DiffDetails} from '../../../api/annotation';
import {resolve} from '../../../models/dependency';
import {browserModelToken} from '../../../models/browser/browser-model';
import {commentsModelToken} from '../../../models/comments/comments-model';
@@ -287,6 +289,10 @@
@state()
private layers: DiffLayer[] = [];
+ private layersComputedWithPlugins = false;
+
+ private enableTokenHighlight?: boolean;
+
@state()
private renderPrefs: RenderPreferences = {
num_lines_rendered_at_once: 128,
@@ -376,6 +382,15 @@
this.prefs = diffPreferences;
}
);
+ subscribe(
+ this,
+ () => this.getPluginLoader().pluginsModel.pluginsLoaded$,
+ async pluginsLoaded => {
+ if (pluginsLoaded) {
+ await this.computeLayersWithPlugins();
+ }
+ }
+ );
}
override connectedCallback() {
@@ -446,6 +461,18 @@
}
}
+ override updated(changedProperties: PropertyValues) {
+ super.updated(changedProperties);
+ const pathChanged = changedProperties.has('path');
+ const diffElementChanged =
+ this._layersComputedForDiffElement !== this.diffElement;
+ const pluginsLoaded =
+ this.getPluginLoader().pluginsModel.getState().pluginsLoaded;
+ if ((pathChanged || diffElementChanged) && pluginsLoaded) {
+ this.computeLayersWithPlugins();
+ }
+ }
+
async waitForReloadToRender(): Promise<void> {
await this.updateComplete;
if (this.reloadPromise) {
@@ -523,13 +550,45 @@
);
}
+ private _layersComputedForPath?: string;
+
+ private _layersComputedForDiffElement?: GrDiff;
+
+ private calculateEnableTokenHighlight(prefs?: PreferencesInfo): boolean {
+ return !prefs?.disable_token_highlighting;
+ }
+
+ private async computeLayersWithPlugins(): Promise<void> {
+ if (
+ this._layersComputedForPath !== this.path ||
+ this._layersComputedForDiffElement !== this.diffElement
+ ) {
+ this.layersComputedWithPlugins = false;
+ this._layersComputedForPath = this.path;
+ this._layersComputedForDiffElement = this.diffElement;
+ }
+
+ if (!this.path || !this.diffElement || this.layersComputedWithPlugins) {
+ return;
+ }
+ if (this.enableTokenHighlight === undefined) {
+ const prefs = await this.restApiService.getPreferences();
+ this.enableTokenHighlight = this.calculateEnableTokenHighlight(prefs);
+ }
+ this.layers = this.getLayers(this.enableTokenHighlight);
+ this.layersComputedWithPlugins = true;
+ }
+
async initLayers() {
const preferencesPromise = this.restApiService.getPreferences();
const prefs = await preferencesPromise;
- const enableTokenHighlight = !prefs?.disable_token_highlighting;
+ this.enableTokenHighlight = this.calculateEnableTokenHighlight(prefs);
assertIsDefined(this.path, 'path');
- this.layers = this.getLayers(enableTokenHighlight);
+ this.layers = this.getLayers(this.enableTokenHighlight);
+ this.layersComputedWithPlugins =
+ !!this.diffElement &&
+ this.getPluginLoader().pluginsModel.getState().pluginsLoaded;
this.coverageRanges = [];
// We kick off fetching the data here, but we don't return the promise,
// so awaiting initLayers() will not wait for coverage data to be
@@ -668,6 +727,26 @@
};
}
+ private get diffDetails(): DiffDetails | undefined {
+ if (
+ !this.change ||
+ !this.patchRange ||
+ !this.file ||
+ !this.path ||
+ !this.diffElement
+ ) {
+ return undefined;
+ }
+ return {
+ change: this.change,
+ basePatchNum: this.patchRange.basePatchNum,
+ patchNum: this.patchRange.patchNum,
+ fileRange: this.file,
+ path: this.path,
+ diffElement: this.diffElement,
+ };
+ }
+
private getLayers(enableTokenHighlight: boolean): DiffLayer[] {
const layers = [];
if (enableTokenHighlight) {
@@ -690,12 +769,29 @@
})
);
}
+ // Add layers from plugins
+ const details = this.diffDetails;
+ if (details) {
+ for (const plugin of this.getPluginLoader().pluginsModel.getState()
+ .diffLayerPlugins) {
+ try {
+ layers.push(plugin.factory(details));
+ } catch (e) {
+ console.error(
+ `Error creating diff layer from plugin ${plugin.pluginName}:`,
+ e
+ );
+ }
+ }
+ }
layers.push(this.syntaxLayer);
return layers;
}
clear() {
this.layers = [];
+ this.layersComputedWithPlugins = false;
+ this.enableTokenHighlight = undefined;
}
/**
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
index 00a627b..958d2fb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
@@ -1474,4 +1474,75 @@
});
});
});
+
+ suite('computeLayersWithPlugins and token highlighting', () => {
+ let getPreferencesStub: sinon.SinonStub;
+ setup(() => {
+ getPreferencesStub = stubRestApi('getPreferences');
+ });
+ test('initLayers sets enableTokenHighlight correctly', async () => {
+ getPreferencesStub.returns(
+ Promise.resolve({disable_token_highlighting: true})
+ );
+ await element.initLayers();
+ assert.isFalse(element['enableTokenHighlight']);
+ getPreferencesStub.returns(
+ Promise.resolve({disable_token_highlighting: false})
+ );
+ await element.initLayers();
+ assert.isTrue(element['enableTokenHighlight']);
+ getPreferencesStub.returns(Promise.resolve(undefined));
+ await element.initLayers();
+ assert.isTrue(element['enableTokenHighlight']);
+ });
+ test('clear resets enableTokenHighlight', async () => {
+ getPreferencesStub.returns(
+ Promise.resolve({disable_token_highlighting: true})
+ );
+ await element.initLayers();
+ assert.isFalse(element['enableTokenHighlight']);
+ element.clear();
+ assert.isUndefined(element['enableTokenHighlight']);
+ });
+ test('computeLayersWithPlugins sets enableTokenHighlight if undefined', async () => {
+ getPreferencesStub.returns(
+ Promise.resolve({disable_token_highlighting: true})
+ );
+ element['enableTokenHighlight'] = undefined;
+ element['layersComputedWithPlugins'] = false;
+
+ await element['computeLayersWithPlugins']();
+
+ assert.isFalse(element['enableTokenHighlight']);
+ assert.isTrue(element['layersComputedWithPlugins']);
+ assert.isTrue(getPreferencesStub.calledOnce);
+ });
+ test('computeLayersWithPlugins does not fetch preferences if enableTokenHighlight is already set', async () => {
+ getPreferencesStub.returns(
+ Promise.resolve({disable_token_highlighting: true})
+ );
+ element['enableTokenHighlight'] = true;
+ element['layersComputedWithPlugins'] = false;
+
+ await element['computeLayersWithPlugins']();
+
+ assert.isTrue(element['enableTokenHighlight']);
+ assert.isTrue(element['layersComputedWithPlugins']);
+ assert.isFalse(getPreferencesStub.called);
+ });
+ test('pluginsLoaded triggers computeLayersWithPlugins', async () => {
+ // @ts-expect-error
+ const computeSpy = sinon.spy(element, 'computeLayersWithPlugins');
+
+ const pluginsModel = testResolver(pluginLoaderToken).pluginsModel;
+ pluginsModel.updateState({pluginsLoaded: false});
+ await element.updateComplete;
+
+ pluginsModel.updateState({pluginsLoaded: true});
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ assert.isTrue(computeSpy.called);
+ });
+ });
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 93bf666..cf151d4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -19,6 +19,8 @@
import '../gr-diff-preferences-dialog/gr-diff-preferences-dialog';
import '../gr-patch-range-select/gr-patch-range-select';
import '../../change/gr-download-dialog/gr-download-dialog';
+import '../../shared/gr-content-with-sidebar/gr-content-with-sidebar';
+import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {getAppContext} from '../../../services/app-context';
import {getParentIndex, isMergeParent} from '../../../utils/patch-set-util';
import {
@@ -88,7 +90,7 @@
import {ifDefined} from 'lit/directives/if-defined.js';
import {classMap} from 'lit/directives/class-map.js';
import {when} from 'lit/directives/when.js';
-import {styleMap} from 'lit/directives/style-map.js';
+import {keyed} from 'lit/directives/keyed.js';
import {
ChangeChildView,
changeViewModelToken,
@@ -127,6 +129,8 @@
@customElement('gr-diff-view')
export class GrDiffView extends LitElement {
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
/**
* Fired when user tries to navigate away while comments are pending save.
*
@@ -153,11 +157,6 @@
@query('#diffPreferencesDialog')
diffPreferencesDialog?: GrDiffPreferencesDialog;
- @query('.sidebarAnchor')
- sidebarAnchor?: HTMLDivElement;
-
- @state() private sidebarHeight = 0;
-
// Private but used in tests.
@state()
get patchRange(): PatchRange | undefined {
@@ -526,7 +525,6 @@
:host {
display: block;
background-color: var(--view-background-color);
- --sidebar-width: 300px;
}
.hidden {
display: none;
@@ -702,24 +700,14 @@
:host(.hideCheckCodePointers) {
--gr-check-code-pointers-display: none;
}
- .diffContainer.sidebarOpen {
- margin-left: var(--sidebar-width);
- }
.sidebarTriggerContainer {
display: inline-block;
margin-right: var(--spacing-m);
}
- .sidebarAnchor {
- height: 0;
- width: 0;
- overflow: visible;
- }
.sidebarContents {
background: var(--background-color-secondary);
- width: var(--sidebar-width);
- border: var(--spacing-xxs) solid var(--border-color);
- border-left: 0;
- overflow: auto;
+ box-sizing: border-box;
+ height: 100%;
}
md-checkbox {
--md-checkbox-container-size: 15px;
@@ -749,8 +737,6 @@
this.addEventListener('open-fix-preview', e => this.onOpenFixPreview(e));
this.cursor = new GrDiffCursor();
if (this.diffHost) this.reInitCursor();
- window.addEventListener('scroll', this.updateSidebarHeight);
- window.addEventListener('resize', this.updateSidebarHeight);
this.getUserModel()
.preferences$.pipe(
map(p => p.diff_page_sidebar),
@@ -768,8 +754,6 @@
override disconnectedCallback() {
this.cursor?.dispose();
- window.removeEventListener('scroll', this.updateSidebarHeight);
- window.removeEventListener('resize', this.updateSidebarHeight);
super.disconnectedCallback();
}
@@ -779,13 +763,6 @@
this.cursor?.reInitCursor();
}
- private readonly updateSidebarHeight = () => {
- if (this.sidebarAnchor) {
- this.sidebarHeight =
- window.innerHeight - this.sidebarAnchor.getBoundingClientRect().bottom;
- }
- };
-
protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
@@ -844,7 +821,6 @@
this.patchRange
);
}
- this.updateSidebarHeight();
}
override render() {
@@ -868,34 +844,41 @@
</div>
`
)}
- <div
- class=${classMap({
- diffContainer: true,
- sidebarOpen: !!this.shownSidebar,
- hidden: !!this.file?.diffs_too_expensive_to_compute,
- })}
+ <gr-content-with-sidebar
+ .side=${this.getSidebarSide()}
+ .hideSide=${!this.shownSidebar}
>
- <gr-endpoint-decorator name="diff-content">
- <gr-diff-host
- id="diffHost"
- .changeNum=${this.changeNum}
- .change=${this.change}
- .patchRange=${this.patchRange}
- .file=${file}
- .lineOfInterest=${this.getLineOfInterest()}
- .path=${this.path}
- .projectName=${this.change?.project}
- @is-blame-loaded-changed=${this.onIsBlameLoadedChanged}
- @comment-anchor-tap=${this.onCommentAnchorTap}
- @line-selected=${this.onLineSelected}
- @diff-changed=${this.onDiffChanged}
- @edit-weblinks-changed=${this.onEditWeblinksChanged}
- @files-weblinks-changed=${this.onFilesWeblinksChanged}
- @render=${this.reInitCursor}
- >
- </gr-diff-host>
- </gr-endpoint-decorator>
- </div>
+ <div
+ slot="main"
+ class=${classMap({
+ diffContainer: true,
+ sidebarOpen: !!this.shownSidebar,
+ hidden: !!this.file?.diffs_too_expensive_to_compute,
+ })}
+ >
+ <gr-endpoint-decorator name="diff-content">
+ <gr-diff-host
+ id="diffHost"
+ .changeNum=${this.changeNum}
+ .change=${this.change}
+ .patchRange=${this.patchRange}
+ .file=${file}
+ .lineOfInterest=${this.getLineOfInterest()}
+ .path=${this.path}
+ .projectName=${this.change?.project}
+ @is-blame-loaded-changed=${this.onIsBlameLoadedChanged}
+ @comment-anchor-tap=${this.onCommentAnchorTap}
+ @line-selected=${this.onLineSelected}
+ @diff-changed=${this.onDiffChanged}
+ @edit-weblinks-changed=${this.onEditWeblinksChanged}
+ @files-weblinks-changed=${this.onFilesWeblinksChanged}
+ @render=${this.reInitCursor}
+ >
+ </gr-diff-host>
+ </gr-endpoint-decorator>
+ </div>
+ <div slot="side">${this.renderSidebarContent()}</div>
+ </gr-content-with-sidebar>
${this.renderDialogs()}
`;
}
@@ -920,7 +903,6 @@
>></a
>
</div>
- ${this.renderSidebarContent()}
</div>`;
}
@@ -1023,17 +1005,33 @@
`;
}
+ private getSidebarSide(): 'left' | 'right' {
+ if (!this.shownSidebar) {
+ return 'left';
+ }
+ const details = this.getPluginLoader().pluginEndPoints.getDetails(
+ `sidebarContent-${this.shownSidebar}`
+ );
+ for (const info of details) {
+ if (info.moduleName) {
+ const customElement = customElements.get(info.moduleName) as
+ | (CustomElementConstructor & {sidebarPosition?: string})
+ | undefined;
+ if (customElement?.sidebarPosition === 'right') {
+ return 'right';
+ }
+ }
+ }
+ return 'left';
+ }
+
private renderSidebarContent() {
- // Always renders the 0x0px .sidebarAnchor div for scroll measurements.
return html`
- <div class="sidebarAnchor">
- ${when(
- this.shownSidebar !== undefined,
- () => html`
- <div
- class="sidebarContents"
- style=${styleMap({height: `${this.sidebarHeight}px`})}
- >
+ ${when(this.shownSidebar !== undefined, () =>
+ keyed(
+ this.shownSidebar,
+ html`
+ <div class="sidebarContents">
<gr-endpoint-decorator
name=${`sidebarContent-${this.shownSidebar}`}
>
@@ -1080,7 +1078,9 @@
// Only close the sidebar if that particular sidebar is
// still open. An async onClose callback should not close a
// different sidebar.
- if (this.shownSidebar !== pluginName) return;
+ if (this.shownSidebar !== pluginName) {
+ return;
+ }
this.shownSidebar = undefined;
this.getUserModel().updatePreferences({
diff_page_sidebar: 'NONE',
@@ -1091,8 +1091,8 @@
</gr-endpoint-decorator>
</div>
`
- )}
- </div>
+ )
+ )}
`;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index b66542e..daed454 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -51,7 +51,9 @@
} from '../../../types/common';
import {CursorMoveResult} from '../../../api/core';
import {Side} from '../../../api/diff';
+import {PluginApi} from '../../../api/plugin';
import {Files, GrDiffView} from './gr-diff-view';
+import {GrContentWithSidebar} from '../../shared/gr-content-with-sidebar/gr-content-with-sidebar';
import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {SinonFakeTimers, SinonStub, SinonStubbedMember} from 'sinon';
import {
@@ -398,14 +400,16 @@
>
</a>
</div>
- <div class="sidebarAnchor"></div>
</div>
<h2 class="assistive-tech-only">Diff view</h2>
- <div class="diffContainer">
- <gr-endpoint-decorator name="diff-content">
- <gr-diff-host id="diffHost"> </gr-diff-host>
- </gr-endpoint-decorator>
- </div>
+ <gr-content-with-sidebar>
+ <div class="diffContainer" slot="main">
+ <gr-endpoint-decorator name="diff-content">
+ <gr-diff-host id="diffHost"> </gr-diff-host>
+ </gr-endpoint-decorator>
+ </div>
+ <div slot="side"></div>
+ </gr-content-with-sidebar>
<gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog>
<gr-diff-preferences-dialog id="diffPreferencesDialog">
</gr-diff-preferences-dialog>
@@ -2261,5 +2265,76 @@
'Button should be hidden for image diffs'
);
});
+
+ suite('sidebar', () => {
+ test('switching sidebars dismounts old component and mounts new one', async () => {
+ // @ts-expect-error: accessing private property shownSidebar for testing
+ element.shownSidebar = 'sidebar-a';
+ await element.updateComplete;
+ const oldDecorator = element.shadowRoot?.querySelector(
+ '.sidebarContents gr-endpoint-decorator'
+ );
+ assert.isNotNull(oldDecorator);
+ assert.equal(
+ oldDecorator?.getAttribute('name'),
+ 'sidebarContent-sidebar-a'
+ );
+
+ // @ts-expect-error: accessing private property shownSidebar for testing
+ element.shownSidebar = 'sidebar-b';
+ await element.updateComplete;
+ const newDecorator = element.shadowRoot?.querySelector(
+ '.sidebarContents gr-endpoint-decorator'
+ );
+ assert.isNotNull(newDecorator);
+ assert.equal(
+ newDecorator?.getAttribute('name'),
+ 'sidebarContent-sidebar-b'
+ );
+ assert.notEqual(oldDecorator, newDecorator);
+ });
+
+ test('defaults to left sidebar side when no sidebarPosition property set', async () => {
+ // @ts-expect-error: accessing private property shownSidebar for testing
+ element.shownSidebar = 'left-sidebar';
+ await element.updateComplete;
+
+ const contentWithSidebar =
+ element.shadowRoot?.querySelector<GrContentWithSidebar>(
+ 'gr-content-with-sidebar'
+ );
+ assert.isNotNull(contentWithSidebar);
+ assert.equal(contentWithSidebar?.side, 'left');
+ });
+
+ test('detects right sidebar side when static sidebarPosition === "right"', async () => {
+ class RightSidebar extends HTMLElement {
+ static sidebarPosition = 'right';
+ }
+
+ customElements.define('right-sidebar-element', RightSidebar);
+
+ let plugin!: PluginApi;
+ window.Gerrit.install(
+ p => (plugin = p),
+ '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js'
+ );
+ plugin.registerDynamicCustomComponent(
+ 'sidebarContent',
+ 'right-sidebar-element'
+ );
+
+ // @ts-expect-error: accessing private property shownSidebar for testing
+ element.shownSidebar = 'testplugin';
+ await element.updateComplete;
+
+ const contentWithSidebar =
+ element.shadowRoot?.querySelector<GrContentWithSidebar>(
+ 'gr-content-with-sidebar'
+ );
+ assert.equal(contentWithSidebar?.side, 'right');
+ });
+ });
});
});
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 7184e00..778a17c 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -6,6 +6,7 @@
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../../shared/gr-button/gr-button';
+import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-editable-label/gr-editable-label';
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../gr-default-editor/gr-default-editor';
@@ -28,6 +29,7 @@
import {changeIsAbandoned, changeIsMerged} from '../../../utils/change-util';
import {Modifier} from '../../../utils/dom-util';
import {sharedStyles} from '../../../styles/shared-styles';
+import {modalStyles} from '../../../styles/gr-modal-styles';
import {css, html, LitElement, nothing, PropertyValues} from 'lit';
import {customElement, query, state} from 'lit/decorators.js';
import {subscribe} from '../../lit/subscription-controller';
@@ -66,6 +68,9 @@
@query('#editPreferencesDialog')
editPreferencesDialog?: GrEditPreferencesDialog;
+ @query('#confirmCloseModal')
+ private confirmCloseModal?: HTMLDialogElement;
+
@state() viewState?: ChangeViewState;
// private but used in test
@@ -171,6 +176,7 @@
static override get styles() {
return [
sharedStyles,
+ modalStyles,
css`
:host {
background-color: var(--view-background-color);
@@ -291,6 +297,18 @@
@has-edit-pref-change-saved=${this.handleEditPrefChangeSaved}
>
</gr-edit-preferences-dialog>
+ <dialog id="confirmCloseModal" tabindex="-1">
+ <gr-dialog
+ id="confirmCloseDialog"
+ confirm-label="Discard"
+ cancel-label="Keep Editing"
+ @confirm=${this.handleConfirmClose}
+ @cancel=${this.handleCancelClose}
+ >
+ <div class="header" slot="header">Discard unsaved changes?</div>
+ <div class="main" slot="main">Your modifications will be lost.</div>
+ </gr-dialog>
+ </dialog>
`;
}
@@ -507,8 +525,11 @@
// private but used in test
handleCloseTap = () => {
- // TODO(kaspern): Add a confirm dialog if there are unsaved changes.
- this.viewEditInChangeView();
+ if ((this.content ?? '') !== this.newContent) {
+ this.confirmCloseModal?.showModal();
+ } else {
+ this.viewEditInChangeView();
+ }
};
private handleSaveTap = () => {
@@ -610,6 +631,16 @@
// We have to fire a reload so the change takes effect within a plugin.
fireReload(this);
}
+
+ private handleConfirmClose = () => {
+ this.getStorage().eraseEditableContentItem(this.storageKey);
+ this.confirmCloseModal?.close();
+ this.viewEditInChangeView();
+ };
+
+ private handleCancelClose = () => {
+ this.confirmCloseModal?.close();
+ };
}
declare global {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_screenshot_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_screenshot_test.ts
new file mode 100644
index 0000000..ec1bd6a
--- /dev/null
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_screenshot_test.ts
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-editor-view';
+import {GrEditorView} from './gr-editor-view';
+import {fixture, html} from '@open-wc/testing';
+// Until https://github.com/modernweb-dev/web/issues/2804 is fixed
+// @ts-ignore
+import {visualDiff} from '@web/test-runner-visual-regression';
+import {
+ query,
+ stubRestApi,
+ visualDiffDarkTheme,
+} from '../../../test/test-utils';
+import {createEditViewState} from '../../../test/test-data-generators';
+import {NumericChangeId, RevisionPatchSetNum} from '../../../types/common';
+import {GrButton} from '../../shared/gr-button/gr-button';
+
+suite('gr-editor-view screenshot tests', () => {
+ let element: GrEditorView;
+
+ setup(async () => {
+ stubRestApi('getFileContent').resolves({
+ ok: true,
+ type: 'text/javascript',
+ content: 'original content',
+ });
+ element = await fixture<GrEditorView>(
+ html`<gr-editor-view></gr-editor-view>`
+ );
+ element.viewState = {
+ ...createEditViewState(),
+ changeNum: 42 as NumericChangeId,
+ patchNum: 1 as RevisionPatchSetNum,
+ editView: {path: 'foo/bar.baz'},
+ };
+ element.latestPatchsetNumber = 1 as RevisionPatchSetNum;
+ element.content = 'original content';
+ element.newContent = 'original content';
+ await element.updateComplete;
+ });
+
+ test('editor view', async () => {
+ await visualDiff(element, 'gr-editor-view-normal');
+ await visualDiffDarkTheme(element, 'gr-editor-view-normal');
+ });
+
+ test('cancel modal open', async () => {
+ element.newContent = 'modified content';
+ await element.updateComplete;
+
+ query<GrButton>(element, '#close')!.click();
+ await element.updateComplete;
+
+ await visualDiff(element, 'gr-editor-view-cancel-modal');
+ await visualDiffDarkTheme(element, 'gr-editor-view-cancel-modal');
+ });
+});
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
index 690180c..090c521 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
@@ -127,6 +127,17 @@
</div>
<gr-edit-preferences-dialog id="editPreferencesDialog">
</gr-edit-preferences-dialog>
+ <dialog id="confirmCloseModal" tabindex="-1">
+ <gr-dialog
+ cancel-label="Keep Editing"
+ confirm-label="Discard"
+ id="confirmCloseDialog"
+ role="dialog"
+ >
+ <div class="header" slot="header">Discard unsaved changes?</div>
+ <div class="main" slot="main">Your modifications will be lost.</div>
+ </gr-dialog>
+ </dialog>
<div class="textareaWrapper">
<gr-endpoint-decorator id="editorEndpoint" name="editor">
<gr-endpoint-param name="fileContent"> </gr-endpoint-param>
@@ -216,6 +227,11 @@
const newText = 'file text changed';
setup(async () => {
+ stubRestApi('getFileContent').resolves({
+ ok: true,
+ type: 'text/javascript',
+ content: originalText,
+ });
element.viewState = {...createEditViewState()};
element.content = originalText;
element.newContent = originalText;
@@ -351,17 +367,89 @@
});
});
- test('file modification and close', async () => {
- const closeSpy = sinon.spy(element, 'handleCloseTap');
+ test('file modification and close show dialog, then discard', async () => {
+ storageService.setEditableContentItem(
+ element.storageKey,
+ 'cached content'
+ );
+ assert.equal(
+ storageService.getEditableContentItem(element.storageKey)?.message,
+ 'cached content'
+ );
+
element.newContent = newText;
await element.updateComplete;
- assert.isFalse(
- query<GrButton>(element, '#save')!.hasAttribute('disabled')
- );
+ const dialog = query<HTMLDialogElement>(element, '#confirmCloseModal')!;
+ assert.isFalse(dialog.hasAttribute('open'));
query<GrButton>(element, '#close')!.click();
- assert.isTrue(closeSpy.called);
+ assert.isFalse(saveFileStub.called);
+ assert.isFalse(navigateStub.called);
+ assert.isTrue(dialog.hasAttribute('open'));
+
+ query(element, '#confirmCloseDialog')!.dispatchEvent(
+ new CustomEvent('confirm')
+ );
+ await element.updateComplete;
+
+ assert.isFalse(dialog.hasAttribute('open'));
+ assert.isTrue(navigateStub.called);
+ assert.isNull(storageService.getEditableContentItem(element.storageKey));
+ });
+
+ test('file modification and close show dialog, then keep editing', async () => {
+ element.newContent = newText;
+ await element.updateComplete;
+
+ const dialog = query<HTMLDialogElement>(element, '#confirmCloseModal')!;
+ assert.isFalse(dialog.hasAttribute('open'));
+
+ query<GrButton>(element, '#close')!.click();
+ assert.isFalse(saveFileStub.called);
+ assert.isFalse(navigateStub.called);
+ assert.isTrue(dialog.hasAttribute('open'));
+
+ query(element, '#confirmCloseDialog')!.dispatchEvent(
+ new CustomEvent('cancel')
+ );
+ await element.updateComplete;
+
+ assert.isFalse(dialog.hasAttribute('open'));
+ assert.isFalse(navigateStub.called);
+ });
+
+ test('render open dialog', async () => {
+ element.newContent = newText;
+ await element.updateComplete;
+
+ query<GrButton>(element, '#close')!.click();
+ await element.updateComplete;
+
+ const dialog = query<HTMLDialogElement>(element, '#confirmCloseModal')!;
+ assert.isTrue(dialog.hasAttribute('open'));
+
+ assert.lightDom.equal(
+ dialog,
+ /* HTML */ `
+ <gr-dialog
+ cancel-label="Keep Editing"
+ confirm-label="Discard"
+ id="confirmCloseDialog"
+ role="dialog"
+ >
+ <div class="header" slot="header">Discard unsaved changes?</div>
+ <div class="main" slot="main">Your modifications will be lost.</div>
+ </gr-dialog>
+ `
+ );
+ });
+
+ test('close when content is not modified', async () => {
+ element.newContent = originalText;
+ await element.updateComplete;
+
+ query<GrButton>(element, '#close')!.click();
assert.isFalse(saveFileStub.called);
assert.isTrue(navigateStub.called);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 53811e9..3020bff 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -355,6 +355,7 @@
sharedStyles,
css`
:host {
+ display: block;
font-family: var(--font-family);
font-size: var(--font-size-normal);
font-weight: var(--font-weight-normal);
@@ -722,10 +723,19 @@
}
override firstUpdated() {
+ const lastComment = this.thread ? this.getLastComment() : undefined;
+ const isNewDraft =
+ isDraft(lastComment) && (lastComment?.message ?? '') === '';
if (this.shouldScrollIntoView) {
whenRendered(this, () => {
this.expandCollapseComments(false);
- this.commentBox?.focus();
+ // Because of the non-deterministic order of focus events firing from
+ // the JS event loop, focusing the comment box on a new draft can result
+ // in the draft comment not being focused, which means the user has to
+ // click into it to start typing.
+ if (!isNewDraft) {
+ this.commentBox?.focus();
+ }
// The delay is a hack because we don't know exactly when to
// scroll the comment into center.
// TODO: Find a better solution without a setTimeout
@@ -735,9 +745,9 @@
}, 500);
});
}
- if (this.thread && isDraft(this.getFirstComment())) {
- const msg = this.getFirstComment()?.message ?? '';
- if (msg.length === 0) this.editDraft();
+ // Focus the draft comment input to avoid the user having to click into it.
+ if (isNewDraft) {
+ this.editDraft();
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 816054b..8b5e166 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -173,6 +173,36 @@
);
});
+ test('focuses commentBox when it is NOT a new draft', async () => {
+ const thread = createThread(c1);
+ const element = await fixture<GrCommentThread>(html`
+ <gr-comment-thread
+ .thread=${thread}
+ .shouldScrollIntoView=${true}
+ ></gr-comment-thread>
+ `);
+ await element.updateComplete;
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ const commentBox = queryAndAssert<HTMLElement>(element, '.comment-box');
+ assert.equal(element.shadowRoot?.activeElement, commentBox);
+ });
+
+ test('does not focus commentBox when it IS a new draft', async () => {
+ const thread = createThread(createNewDraft({message: ''}));
+ const element = await fixture<GrCommentThread>(html`
+ <gr-comment-thread
+ .thread=${thread}
+ .shouldScrollIntoView=${true}
+ ></gr-comment-thread>
+ `);
+ await element.updateComplete;
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ const commentBox = queryAndAssert<HTMLElement>(element, '.comment-box');
+ assert.notEqual(element.shadowRoot?.activeElement, commentBox);
+ });
+
test('comment box spans 100% of container width', async () => {
const wrapper = await fixture(html`
<div style="width: 500px;">
@@ -518,6 +548,85 @@
// The draft should be discarded completely
assert.equal(draftElement.messageText, '');
});
+
+ test('handle Quote with multi-line message', async () => {
+ stubAdd.restore();
+ stubAdd = sinon
+ .stub(testResolver(commentsModelToken), 'addNewDraft')
+ .callsFake(draft => {
+ const newDraft = {
+ ...draft,
+ id: 'new-draft' as UrlEncodedCommentId,
+ __draft: true,
+ };
+ if (element.thread) {
+ element.thread = {
+ ...element.thread,
+ comments: [...element.thread.comments, newDraft],
+ };
+ }
+ return Promise.resolve(newDraft);
+ });
+
+ element.thread = createThread(c1, {
+ ...c2,
+ message: 'first line\nsecond line\nthird line',
+ unresolved: true,
+ });
+ await element.updateComplete;
+
+ queryAndAssert<GrButton>(element, '#quoteBtn').click();
+ assert.isTrue(stubAdd.called);
+ assert.equal(stubAdd.lastCall.firstArg.in_reply_to, c2.id);
+ await element.updateComplete;
+
+ const draftElement = queryAndAssert<GrComment>(
+ element,
+ 'gr-comment.draft'
+ );
+ await draftElement.updateComplete;
+ await waitUntil(
+ () =>
+ draftElement.messageText ===
+ '> first line\n> second line\n> third line\n\n'
+ );
+ assert.equal(
+ draftElement.messageText,
+ '> first line\n> second line\n> third line\n\n'
+ );
+ });
+
+ test('handle reply-to-comment event from child comment', async () => {
+ element.thread = createThread(c1, {...c2, unresolved: true});
+ await element.updateComplete;
+
+ const commentEl = queryAndAssert<GrComment>(element, 'gr-comment');
+ commentEl.dispatchEvent(
+ new CustomEvent('reply-to-comment', {
+ detail: {
+ content: 'custom response',
+ userWantsToEdit: true,
+ unresolved: true,
+ },
+ bubbles: true,
+ composed: true,
+ })
+ );
+
+ assert.isTrue(stubAdd.called);
+ assert.equal(stubAdd.lastCall.firstArg.in_reply_to, c2.id);
+ assert.equal(stubAdd.lastCall.firstArg.unresolved, true);
+ });
+
+ test('reply sets in_reply_to to the last comment id in thread', async () => {
+ element.thread = createThread(c1, c2);
+ await element.updateComplete;
+
+ queryAndAssert<GrButton>(element, '#replyBtn').click();
+ assert.isTrue(stubAdd.called);
+ const newDraft = stubAdd.lastCall.firstArg;
+ assert.equal(newDraft.in_reply_to, c2.id);
+ });
});
test('comments are sorted correctly', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 418ddb0..24ca939 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -1381,11 +1381,9 @@
override updated(changed: PropertyValues) {
if (changed.has('editing')) {
if (this.editing && !this.permanentEditingMode) {
- // Note that this is a bit fragile, because we are relying on the
- // comment to become visible soonish. If that does not happen, then we
- // will be waiting indefinitely and grab focus at some point in the
- // distant future.
- whenVisible(this, () => this.textarea?.putCursorAtEnd());
+ this.focusTextarea().catch(() => {
+ // Ignore error since failure to focus is non-fatal.
+ });
}
}
if (changed.has('changeNum') || changed.has('comment')) {
@@ -1400,6 +1398,27 @@
}
}
+ private async focusTextarea(): Promise<void> {
+ await this.updateComplete;
+ if (!this.textarea) {
+ return;
+ }
+ await this.textarea.updateComplete;
+ if (this.isVisible()) {
+ this.textarea.putCursorAtEnd();
+ } else {
+ // Note that this is a bit fragile, because we are relying on the
+ // comment to become visible soonish. If that does not happen, then we
+ // will be waiting indefinitely and grab focus at some point in the
+ // distant future.
+ whenVisible(this, () => this.textarea?.putCursorAtEnd());
+ }
+ }
+
+ private isVisible(): boolean {
+ return this.offsetWidth > 0 || this.offsetHeight > 0;
+ }
+
override willUpdate(changed: PropertyValues) {
this.firstWillUpdate();
if (changed.has('comment')) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 58a64bd..b437a74 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -54,6 +54,7 @@
import {suggestionsServiceToken} from '../../../services/suggestions/suggestions-service';
import {AutocompletionContext} from '../../../utils/autocomplete-cache';
import {FixSuggestionInfo} from '../../../api/rest-api';
+import {GrSuggestionTextarea} from '../gr-suggestion-textarea/gr-suggestion-textarea';
suite('gr-comment tests', () => {
let element: GrComment;
@@ -533,6 +534,24 @@
assert.isTrue(element.isSaveDisabled());
});
+ test('focuses textarea when editing is set to true', async () => {
+ const spy = sinon.spy(GrSuggestionTextarea.prototype, 'putCursorAtEnd');
+ try {
+ element.comment = createDraft();
+ element.editing = false;
+ await element.updateComplete;
+
+ element.editing = true;
+ await element.updateComplete;
+ // focusTextarea is async, wait for it to complete.
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ assert.isTrue(spy.called);
+ } finally {
+ spy.restore();
+ }
+ });
+
test('ctrl+s saves comment', async () => {
const spy = sinon.stub(element, 'save');
element.messageText = 'is that the horse from horsing around??';
diff --git a/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar.ts b/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar.ts
index 0e0638e..869f9df 100644
--- a/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar.ts
+++ b/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar.ts
@@ -4,14 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {customElement, property, query, state} from 'lit/decorators.js';
-import {css, html, LitElement} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
import {styleMap} from 'lit/directives/style-map.js';
-const SIDEBAR_MIN_WIDTH = 400;
+const SIDEBAR_MIN_WIDTH = 250;
/**
* A component that displays content in a main area and a resizable sidebar.
- * The sidebar can be toggled between hidden and visible.
+ * The sidebar can be toggled between hidden and visible and positioned on the left or right.
*
* slot main - The content to be displayed in the main area.
* slot side - The content to be displayed in the sidebar.
@@ -21,11 +21,17 @@
@query('.sidebar-wrapper') sidebarWrapper?: HTMLElement;
@state()
- private sidebarWidthPx = SIDEBAR_MIN_WIDTH;
+ private sidebarWidthPx = 400;
@property()
hideSide = true;
+ @property()
+ side: 'left' | 'right' = 'right';
+
+ @property({type: Number})
+ minWidth = SIDEBAR_MIN_WIDTH;
+
private isSidebarResizing = false;
private sidebarResizingStartPosPx = 0;
@@ -43,22 +49,29 @@
:host {
display: block;
position: relative;
- --sidebar-height: calc(100vh - var(--sidebar-top));
+ --sidebar-height: calc(100vh - var(--sidebar-top, 0px));
}
.sidebar-wrapper {
z-index: 50;
position: absolute;
display: flex;
top: 0;
- bottom: calc(0px - var(--sidebar-bottom-overflow));
- right: 0;
- min-width: 400px;
+ bottom: calc(0px - var(--sidebar-bottom-overflow, 0px));
+ min-width: 250px;
max-width: 100%;
background-color: var(--background-color-secondary);
}
+ .sidebar-wrapper.right {
+ right: 0;
+ left: auto;
+ }
+ .sidebar-wrapper.left {
+ left: 0;
+ right: auto;
+ }
.sidebar {
position: sticky;
- top: var(--sidebar-top);
+ top: var(--sidebar-top, 0px);
height: var(--sidebar-height);
box-sizing: border-box;
overflow: auto;
@@ -67,35 +80,55 @@
}
.resizer-wrapper {
position: sticky;
- top: var(--sidebar-top);
+ top: var(--sidebar-top, 0px);
height: var(--sidebar-height);
z-index: 51;
}
.resizer {
background-color: var(--background-color-secondary);
width: 7px;
- border-left: 1px solid var(--border-color);
cursor: ew-resize;
position: absolute;
top: 0;
bottom: 0;
- left: -7px;
box-sizing: border-box;
}
- .resizer:hover {
+ .resizer.right-side {
+ left: -7px;
+ border-left: 1px solid var(--border-color);
+ }
+ .resizer.right-side:hover {
background-color: var(--background-color-tertiary);
width: 11px;
left: -9px;
}
+ .resizer.left-side {
+ right: -7px;
+ border-right: 1px solid var(--border-color);
+ }
+ .resizer.left-side:hover {
+ background-color: var(--background-color-tertiary);
+ width: 11px;
+ right: -9px;
+ }
`,
];
}
override render() {
const widthPx = this.hideSide ? 0 : this.sidebarWidthPx;
+ const mainStyle =
+ this.side === 'left'
+ ? styleMap({
+ marginLeft: `${widthPx}px`,
+ width: `calc(100% - ${widthPx}px)`,
+ })
+ : styleMap({
+ width: `calc(100% - ${widthPx}px)`,
+ });
return html`
<div>
- <div style=${styleMap({width: `calc(100% - ${widthPx}px)`})}>
+ <div style=${mainStyle}>
<slot name="main"></slot>
</div>
${this.renderSidebar()}
@@ -105,25 +138,34 @@
private renderSidebar() {
if (this.hideSide) return;
+ const sideClass = this.side === 'left' ? 'left' : 'right';
+ const resizerClass = this.side === 'left' ? 'left-side' : 'right-side';
return html`
<div
- class="sidebar-wrapper"
+ class="sidebar-wrapper ${sideClass}"
style=${styleMap({width: `${this.sidebarWidthPx}px`})}
>
- <div class="resizer-wrapper">
- <div
- class="resizer"
- role="separator"
- aria-orientation="vertical"
- aria-valuenow=${this.sidebarWidthPx}
- aria-label="Resize sidebar"
- tabindex="0"
- @mousedown=${this.startSidebarResize}
- ></div>
- </div>
+ ${this.side === 'right' ? this.renderResizer(resizerClass) : nothing}
<div class="sidebar">
<slot name="side"></slot>
</div>
+ ${this.side === 'left' ? this.renderResizer(resizerClass) : nothing}
+ </div>
+ `;
+ }
+
+ private renderResizer(resizerClass: string) {
+ return html`
+ <div class="resizer-wrapper">
+ <div
+ class="resizer ${resizerClass}"
+ role="separator"
+ aria-orientation="vertical"
+ aria-valuenow=${this.sidebarWidthPx}
+ aria-label="Resize sidebar"
+ tabindex="0"
+ @mousedown=${this.startSidebarResize}
+ ></div>
</div>
`;
}
@@ -157,10 +199,11 @@
if (!this.isSidebarResizing || event.buttons === 0) return;
const widthDiffPx = event.clientX - this.sidebarResizingStartPosPx;
- this.sidebarWidthPx = Math.max(
- this.sidebarResizingStartWidthPx - widthDiffPx,
- SIDEBAR_MIN_WIDTH
- );
+ const rawWidth =
+ this.side === 'right'
+ ? this.sidebarResizingStartWidthPx - widthDiffPx
+ : this.sidebarResizingStartWidthPx + widthDiffPx;
+ this.sidebarWidthPx = Math.max(rawWidth, this.minWidth);
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar_screenshot_test.ts b/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar_screenshot_test.ts
index 52ea31d..7ed19b7 100644
--- a/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar_screenshot_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar_screenshot_test.ts
@@ -44,8 +44,23 @@
await element.updateComplete;
});
- test('screenshot', async () => {
- await visualDiff(wrapper, 'gr-content-with-sidebar');
- await visualDiffDarkTheme(wrapper, 'gr-content-with-sidebar');
+ test('screenshot right sidebar', async () => {
+ const element = wrapper.querySelector<GrContentWithSidebar>(
+ 'gr-content-with-sidebar'
+ )!;
+ element.side = 'right';
+ await element.updateComplete;
+ await visualDiff(wrapper, 'gr-content-with-sidebar-right');
+ await visualDiffDarkTheme(wrapper, 'gr-content-with-sidebar-right');
+ });
+
+ test('screenshot left sidebar', async () => {
+ const element = wrapper.querySelector<GrContentWithSidebar>(
+ 'gr-content-with-sidebar'
+ )!;
+ element.side = 'left';
+ await element.updateComplete;
+ await visualDiff(wrapper, 'gr-content-with-sidebar-left');
+ await visualDiffDarkTheme(wrapper, 'gr-content-with-sidebar-left');
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar_test.ts b/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar_test.ts
index 7905c0a..626e3dc 100644
--- a/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-content-with-sidebar/gr-content-with-sidebar_test.ts
@@ -34,8 +34,9 @@
);
});
- test('renders sidebar', async () => {
+ test('renders right sidebar', async () => {
element.hideSide = false;
+ element.side = 'right';
await element.updateComplete;
assert.shadowDom.equal(
@@ -45,13 +46,13 @@
<div style="width: calc(100% - 400px);">
<slot name="main"> </slot>
</div>
- <div class="sidebar-wrapper" style="width:400px;">
+ <div class="right sidebar-wrapper" style="width:400px;">
<div class="resizer-wrapper">
<div
aria-label="Resize sidebar"
aria-orientation="vertical"
aria-valuenow="400"
- class="resizer"
+ class="right-side resizer"
role="separator"
tabindex="0"
></div>
@@ -64,4 +65,36 @@
`
);
});
+
+ test('renders left sidebar', async () => {
+ element.hideSide = false;
+ element.side = 'left';
+ await element.updateComplete;
+
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <div>
+ <div style="width: calc(100% - 400px); margin-left: 400px;">
+ <slot name="main"> </slot>
+ </div>
+ <div class="left sidebar-wrapper" style="width:400px;">
+ <div class="sidebar">
+ <slot name="side"> </slot>
+ </div>
+ <div class="resizer-wrapper">
+ <div
+ aria-label="Resize sidebar"
+ aria-orientation="vertical"
+ aria-valuenow="400"
+ class="left-side resizer"
+ role="separator"
+ tabindex="0"
+ ></div>
+ </div>
+ </div>
+ </div>
+ `
+ );
+ });
});
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 0ee4aa3..bfc6462 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -52,6 +52,9 @@
@property({type: Boolean, reflect: true})
hideInput = false;
+ @property({type: Boolean, attribute: 'disable-auto-select', reflect: true})
+ disableAutoSelect = false;
+
@property({type: String})
label?: string;
@@ -216,6 +219,7 @@
}
private handleInputClick(e: MouseEvent) {
+ if (this.disableAutoSelect) return;
e.preventDefault();
const rootTarget = e.composedPath()[0];
(rootTarget as HTMLInputElement).select();
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
index b50ecfd..acc1180 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.ts
@@ -128,6 +128,22 @@
assert.equal(mdOutlinedTextField.selectionEnd, element.text!.length - 1);
});
+ test('handleInputClick with disableAutoSelect', async () => {
+ element.disableAutoSelect = true;
+ await element.updateComplete;
+
+ const mdOutlinedTextField = queryAndAssert<MdOutlinedTextField>(
+ element,
+ 'md-outlined-text-field'
+ );
+ mdOutlinedTextField.selectionStart = 0;
+ mdOutlinedTextField.selectionEnd = 0;
+
+ mdOutlinedTextField.click();
+ assert.equal(mdOutlinedTextField.selectionStart, 0);
+ assert.equal(mdOutlinedTextField.selectionEnd, 0);
+ });
+
test('hideInput', async () => {
const mdOutlinedTextField = queryAndAssert<MdOutlinedTextField>(
element,
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 9a7cabc..30defc5 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -54,6 +54,9 @@
@property({type: Boolean, attribute: 'show-keyboard-shortcut-tooltips'})
showKeyboardShortcutTooltips = false;
+ @property({type: Boolean, attribute: 'disable-auto-select', reflect: true})
+ disableAutoSelect = false;
+
// Private but used in tests.
readonly getUserModel = resolve(this, userModelToken);
@@ -171,6 +174,7 @@
.label=${command.title}
.command=${command.command}
.tooltip=${this.computeTooltip(index)}
+ .disableAutoSelect=${this.disableAutoSelect}
></gr-shell-command>
`;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
index 05011f8f..3f3b4bc 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.ts
@@ -97,6 +97,16 @@
assert.isTrue(focusStub.called);
});
+ test('passes disableAutoSelect to gr-shell-command', async () => {
+ element.disableAutoSelect = true;
+ await element.updateComplete;
+ const shellCommand = queryAndAssert<GrShellCommand>(
+ element,
+ 'gr-shell-command'
+ );
+ assert.isTrue(shellCommand.disableAutoSelect);
+ });
+
test('element visibility', async () => {
assert.isFalse(isHidden(queryAndAssert(element, 'md-tabs')));
assert.isFalse(isHidden(queryAndAssert(element, '.commands')));
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index 6c2661f..1261e29 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -337,7 +337,6 @@
this.value = this.inputText || '';
}
this.editing = false;
- // TODO: This event seems to be unused (no listener). Remove?
fire(this, 'changed', this.value);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index 95b6851..917ed1d 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -119,7 +119,7 @@
white-space: normal;
/* prose will automatically wrap but inline <code> blocks won't and we
should overflow in that case rather than wrapping or leaking out */
- overflow-x: auto;
+ overflow-x: var(--gr-formatted-text-markdown-html-overflow-x, auto);
overflow-wrap: break-word;
}
`,
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
index 0250d82..5abf4fc 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
@@ -6,6 +6,7 @@
import {
AnnotationPluginApi,
CoverageProvider,
+ DiffLayerFactory,
TokenHoverListener,
} from '../../../api/annotation';
import {PluginApi} from '../../../api/plugin';
@@ -36,4 +37,12 @@
listener,
});
}
+
+ addDiffLayer(factory: DiffLayerFactory): void {
+ this.reporting.trackApi(this.plugin, 'annotation', 'addDiffLayer');
+ this.pluginsModel.diffLayerRegister({
+ pluginName: this.plugin.getPluginName(),
+ factory,
+ });
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 6ee3bf3..145be54 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -217,6 +217,11 @@
// only skip the check if the caller wants to prosess status themselves.
reportServerError?: boolean;
isHighPriority?: boolean;
+ /**
+ * If true, the request will be routed through the readScheduler even if it
+ * uses a write method (PUT, POST, DELETE).
+ */
+ useReadScheduler?: boolean;
}
export interface FetchOptionsInit {
@@ -264,12 +269,20 @@
private readonly writeScheduler: Scheduler<Response>
) {}
+ private isWrite(method: string, useReadScheduler?: boolean): boolean {
+ return (
+ (method === 'PUT' || method === 'POST' || method === 'DELETE') &&
+ !useReadScheduler
+ );
+ }
+
private schedule(
method: string,
task: Task<Response>,
- name?: string
+ name?: string,
+ useReadScheduler?: boolean
): Promise<Response> {
- if (method === 'PUT' || method === 'POST' || method === 'DELETE') {
+ if (this.isWrite(method, useReadScheduler)) {
return this.writeScheduler.schedule(task, name);
} else {
return this.readScheduler.schedule(task, name);
@@ -284,8 +297,7 @@
const method = req.fetchOptions?.method ?? HttpMethod.GET;
const startTime = Date.now();
- const isWrite =
- method === 'PUT' || method === 'POST' || method === 'DELETE';
+ const isWrite = this.isWrite(method, req.useReadScheduler);
const origin = req.fetchOptions?.headers?.get(REQUEST_ORIGIN_HEADER);
const pluginName = origin?.startsWith('plugin:') ? origin : undefined;
const requestName = `${method} - ${
@@ -330,7 +342,9 @@
};
const resPromise = (
- req.isHighPriority ? task() : this.schedule(method, task, requestName)
+ req.isHighPriority
+ ? task()
+ : this.schedule(method, task, requestName, req.useReadScheduler)
).catch((err: unknown) => {
if (err instanceof RetryError) {
return err.payload;
@@ -414,6 +428,9 @@
fetchOptions: req.fetchOptions,
anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
};
+ if (req.useReadScheduler !== undefined) {
+ fetchReq.useReadScheduler = req.useReadScheduler;
+ }
let resp: Response;
try {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
index 92feb24..ea79611 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.ts
@@ -130,6 +130,33 @@
});
assert.equal(readScheduler.scheduled.length, 0);
await assertWriteRequest();
+ const res = await promise;
+ assert.equal(await res.text(), 'Yay');
+ });
+
+ test('POST are sent to writeScheduler', async () => {
+ const promise = helper.fetch({
+ fetchOptions: {
+ method: HttpMethod.POST,
+ },
+ url: '/dummy/url',
+ });
+ assert.equal(readScheduler.scheduled.length, 0);
+ await assertWriteRequest();
+ const res = await promise;
+ assert.equal(await res.text(), 'Yay');
+ });
+
+ test('POST with useReadScheduler are sent to readScheduler', async () => {
+ const promise = helper.fetch({
+ fetchOptions: {
+ method: HttpMethod.POST,
+ },
+ url: '/changes/123/revisions/1/fix:preview',
+ useReadScheduler: true,
+ });
+ assert.equal(writeScheduler.scheduled.length, 0);
+ await assertReadRequest();
const res: Response = await promise;
assert.equal(await res.text(), 'Yay');
});
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
index 0c9be74..54aecb3 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
@@ -27,6 +27,9 @@
@property({type: String})
tooltip = '';
+ @property({type: Boolean, attribute: 'disable-auto-select', reflect: true})
+ disableAutoSelect = false;
+
static override get styles() {
return [
sharedStyles,
@@ -73,6 +76,7 @@
.text=${this.command}
hasTooltip
buttonTitle=${this.tooltip}
+ .disableAutoSelect=${this.disableAutoSelect}
></gr-copy-clipboard>
</div>`;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
index 1e0ff11..8fb66f5 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.ts
@@ -44,4 +44,14 @@
await element.focusOnCopy();
assert.isTrue(focusStub.called);
});
+
+ test('passes disableAutoSelect to gr-copy-clipboard', async () => {
+ element.disableAutoSelect = true;
+ await element.updateComplete;
+ const copyClipboard = queryAndAssert<GrCopyClipboard>(
+ element,
+ 'gr-copy-clipboard'
+ );
+ assert.isTrue(copyClipboard.disableAutoSelect);
+ });
});
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
index 72e0905..0b46706 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
@@ -30,7 +30,11 @@
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {fire, fireError} from '../../../utils/event-util';
import {Timing} from '../../../constants/reporting';
-import {createChangeUrl} from '../../../models/views/change';
+import {
+ changeViewModelToken,
+ createApplyFixUrl,
+} from '../../../models/views/change';
+
import {getFileExtension} from '../../../utils/file-util';
import {throwingErrorCallback} from '../gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {ReportSource} from '../../../services/suggestions/suggestions-service';
@@ -128,6 +132,8 @@
private readonly getNavigation = resolve(this, navigationToken);
+ private readonly getViewModel = resolve(this, changeViewModelToken);
+
private readonly syntaxLayer = new GrSyntaxLayerWorker(
resolve(this, highlightServiceToken),
() => getAppContext().reportingService
@@ -391,16 +397,23 @@
// basePatchNum is from comment patchset and comment cannot be created
// in EDIT. RevisionPatchset without EDIT is PatchSetNumber
if (res?.ok && basePatchNum !== undefined && basePatchNum !== EDIT) {
+ const currentChildView = this.getViewModel().getState()?.childView;
+ const filePath =
+ fixSuggestion.replacements[0]?.path ?? this.preview?.filepath;
this.getNavigation().setUrl(
- createChangeUrl({
+ createApplyFixUrl({
changeNum,
repo: this.repo!,
- patchNum: EDIT,
basePatchNum: basePatchNum as PatchSetNumber,
forceReload: !this.hasEdit,
+ filePath,
+ currentChildView,
})
);
- fire(this, 'reload-diff', {path: fixSuggestion.replacements[0].path});
+
+ if (filePath) {
+ fire(this, 'reload-diff', {path: filePath});
+ }
fire(this, 'apply-user-suggestion', {
fixSuggestion: fixSuggestion.description.includes(
ReportSource.GET_AI_FIX_FOR_COMMENT
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
index 742900f..da0220c 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
@@ -17,8 +17,21 @@
} from '../../../test/test-data-generators';
import {getAppContext} from '../../../services/app-context';
import {GrSuggestionDiffPreview} from './gr-suggestion-diff-preview';
-import {stubFlags} from '../../../test/test-utils';
-import {NumericChangeId, RevisionPatchSetNum} from '../../../api/rest-api';
+import * as sinon from 'sinon';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {stubFlags, stubRestApi} from '../../../test/test-utils';
+import {
+ NumericChangeId,
+ RepoName,
+ RevisionPatchSetNum,
+} from '../../../api/rest-api';
+import {changeViewModelToken} from '../../../models/views/change';
+import {
+ createChangeViewState,
+ createDiffViewState,
+ createRange,
+} from '../../../test/test-data-generators';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-suggestion-diff-preview tests', () => {
let element: GrSuggestionDiffPreview;
@@ -121,4 +134,53 @@
{ignoreAttributes: ['style']}
);
});
+
+ suite('applyFix navigation', () => {
+ let setUrlStub: sinon.SinonStub;
+
+ setup(() => {
+ setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
+ stubRestApi('applyFixSuggestion').returns(
+ Promise.resolve(new Response(null, {status: 200}))
+ );
+ element.changeNum = 42 as NumericChangeId;
+ element.repo = 'test-project' as RepoName;
+ element.patchSet = 1 as RevisionPatchSetNum;
+
+ element.fixSuggestionInfo = {
+ ...createFixSuggestionInfo(),
+ replacements: [
+ {
+ path: 'foo/bar.ts',
+ replacement: 'new content',
+ range: createRange(),
+ },
+ ],
+ };
+ });
+
+ test('navigates to createDiffUrl when in Diff View', async () => {
+ testResolver(changeViewModelToken).setState(createDiffViewState());
+
+ await element.applyFix();
+
+ assert.isTrue(setUrlStub.calledOnce);
+ assert.equal(
+ setUrlStub.lastCall.firstArg,
+ '/c/test-project/+/42/1..edit/foo/bar.ts?forceReload=true'
+ );
+ });
+
+ test('navigates to createChangeUrl when in Change View', async () => {
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+
+ await element.applyFix();
+
+ assert.isTrue(setUrlStub.calledOnce);
+ assert.equal(
+ setUrlStub.lastCall.firstArg,
+ '/c/test-project/+/42/1..edit?forceReload=true'
+ );
+ });
+ });
});
diff --git a/polygerrit-ui/app/models/checks/checks-model.ts b/polygerrit-ui/app/models/checks/checks-model.ts
index c986efe..3fbc552 100644
--- a/polygerrit-ui/app/models/checks/checks-model.ts
+++ b/polygerrit-ui/app/models/checks/checks-model.ts
@@ -321,9 +321,18 @@
);
this.checksLatest$ = select(this.state$, state => state.pluginStateLatest);
this.checksSelected$ = select(
- combineLatest([this.state$, this.changeViewModel.checksPatchset$]),
- ([state, ps]) => {
- const checksPs = ps ? ChecksPatchset.SELECTED : ChecksPatchset.LATEST;
+ combineLatest([
+ this.state$,
+ this.changeViewModel.checksPatchset$,
+ this.changeModel.latestPatchNum$,
+ ]),
+ ([state, ps, latestPs]) => {
+ // When no distinct patchset is selected SELECTED fetch is skipped
+ // (see initFetchingOfData), so fall back to the LATEST state here.
+ const checksPs =
+ ps && ps !== latestPs
+ ? ChecksPatchset.SELECTED
+ : ChecksPatchset.LATEST;
return this.getPluginState(state, checksPs);
}
);
@@ -865,29 +874,54 @@
this.reloadSubjects[pluginName],
pollIntervalMs === 0 ? from([0]) : timer(0, pollIntervalMs),
this.documentVisibilityChange$,
+ // Only the SELECTED subscription needs the latest patchset here, to
+ // detect when it coincides with the latest (see below). The LATEST
+ // subscription already has it as its second element, so feeding it in
+ // again would make combineLatest emit twice per change.
+ patchset === ChecksPatchset.SELECTED
+ ? this.changeModel.latestPatchNum$
+ : of(undefined),
])
.pipe(
takeWhile(_ => !!this.providers[pluginName]),
filter(_ => document.visibilityState !== 'hidden'),
throttleTime(500, undefined, {leading: true, trailing: true}),
- switchMap(([change, patchNum]): Observable<FetchResponse> => {
- if (!change || !patchNum) return of(this.empty());
- if (typeof patchNum !== 'number') return of(this.empty());
- assertIsDefined(change.revisions, 'change.revisions');
- const patchsetSha = getShaByPatchNum(change.revisions, patchNum);
- // Sometimes patchNum is updated earlier than change, so change
- // revisions don't have patchNum yet
- if (!patchsetSha) return of(this.empty());
- const data: ChangeData = {
- changeNumber: change?._number,
- patchsetNumber: patchNum,
- patchsetSha,
- repo: change.project,
- commitMessage: getCurrentRevision(change)?.commit?.message,
- changeInfo: change as ChangeInfo,
- };
- return this.fetchResults(pluginName, data, patchset);
- }),
+ switchMap(
+ ([
+ change,
+ patchNum,
+ ,
+ ,
+ ,
+ latestPatchNum,
+ ]): Observable<FetchResponse> => {
+ if (!change || !patchNum) return of(this.empty());
+ if (typeof patchNum !== 'number') return of(this.empty());
+ // Skip the duplicate fetch when the selected patchset is the
+ // latest: the LATEST subscription already fetches it and
+ // checksSelected$ falls back to that state.
+ if (
+ patchset === ChecksPatchset.SELECTED &&
+ patchNum === latestPatchNum
+ ) {
+ return of(this.empty());
+ }
+ assertIsDefined(change.revisions, 'change.revisions');
+ const patchsetSha = getShaByPatchNum(change.revisions, patchNum);
+ // Sometimes patchNum is updated earlier than change, so change
+ // revisions don't have patchNum yet
+ if (!patchsetSha) return of(this.empty());
+ const data: ChangeData = {
+ changeNumber: change?._number,
+ patchsetNumber: patchNum,
+ patchsetSha,
+ repo: change.project,
+ commitMessage: getCurrentRevision(change)?.commit?.message,
+ changeInfo: change as ChangeInfo,
+ };
+ return this.fetchResults(pluginName, data, patchset);
+ }
+ ),
catchError(e => {
// This should not happen and is really severe, because it means that
// the Observable has terminated and we won't recover from that. No
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index 94b3d0f..368b7e6 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -17,6 +17,7 @@
import {
Action,
Category,
+ ChangeData,
CheckRun,
ChecksApiConfig,
ChecksProvider,
@@ -27,9 +28,11 @@
import {
createCheckResult,
createParsedChange,
+ createRevisions,
createRun,
+ getCurrentRevision,
} from '../../test/test-data-generators';
-import {waitUntil, waitUntilCalled} from '../../test/test-utils';
+import {waitEventLoop, waitUntil, waitUntilCalled} from '../../test/test-utils';
import {ParsedChangeInfo} from '../../types/types';
import {
changeModelToken,
@@ -38,7 +41,11 @@
import {assert} from '@open-wc/testing';
import {testResolver} from '../../test/common-test-setup';
import {changeViewModelToken} from '../views/change';
-import {NumericChangeId, PatchSetNumber} from '../../api/rest-api';
+import {
+ NumericChangeId,
+ PatchSetNumber,
+ RevisionPatchSetNum,
+} from '../../api/rest-api';
import {pluginLoaderToken} from '../../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {deepEqual} from '../../utils/deep-util';
@@ -79,6 +86,29 @@
};
}
+/**
+ * A provider that echoes back the patchset it was asked to fetch, so that tests
+ * can assert which patchset a tab is populated with.
+ */
+function createPatchsetTaggingProvider(): ChecksProvider {
+ return {
+ fetch: (data: ChangeData) =>
+ Promise.resolve({
+ responseCode: ResponseCode.OK,
+ runs: [createRun({patchset: data.patchsetNumber})],
+ }),
+ };
+}
+
+/** A change with two patchsets, so latest (2) and older (1) are distinct. */
+function createTwoPatchsetChange(): ParsedChangeInfo {
+ return updateRevisionsWithCommitShas({
+ ...createParsedChange(),
+ revisions: createRevisions(2),
+ current_revision: getCurrentRevision(1),
+ })!;
+}
+
suite('checks-model tests', () => {
let model: ChecksModel;
@@ -130,6 +160,130 @@
clock.restore();
});
+ test('no duplicate fetch when viewing latest patchset (no selection)', async () => {
+ const clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
+ let change: ParsedChangeInfo | undefined = undefined;
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
+ let latestRuns: CheckRun[] = [];
+ let selectedRuns: CheckRun[] = [];
+ model.allRunsLatestPatchset$.subscribe(r => (latestRuns = r));
+ model.allRunsSelectedPatchset$.subscribe(r => (selectedRuns = r));
+ const provider = createPatchsetTaggingProvider();
+ const fetchSpy = sinon.spy(provider, 'fetch');
+
+ model.register({
+ pluginName: PLUGIN_NAME,
+ provider,
+ config: CONFIG_POLLING_NONE,
+ });
+ await waitUntil(() => change === undefined);
+
+ // Viewing the latest patchset (2), no explicit checks patchset override.
+ testResolver(changeViewModelToken).updateState({
+ patchNum: 2 as RevisionPatchSetNum,
+ });
+ const testChange = createTwoPatchsetChange();
+ testResolver(changeModelToken).updateStateChange(testChange);
+ await waitUntil(() => deepEqual(change, testChange));
+
+ // Fire the throttled emission, then flush the fetch promise into state.
+ clock.tick(600);
+ await waitEventLoop();
+
+ // The SELECTED patchset equals LATEST, so only a single fetch is needed.
+ assert.equal(fetchSpy.callCount, 1);
+ // Both tabs show data for the latest patchset (2).
+ assert.isNotEmpty(latestRuns);
+ assert.isNotEmpty(selectedRuns);
+ assert.isTrue(latestRuns.every(r => r.patchset === 2));
+ assert.isTrue(selectedRuns.every(r => r.patchset === 2));
+
+ clock.restore();
+ });
+
+ test('no duplicate fetch when latest patchset is explicitly selected', async () => {
+ const clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
+ let change: ParsedChangeInfo | undefined = undefined;
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
+ let latestRuns: CheckRun[] = [];
+ let selectedRuns: CheckRun[] = [];
+ model.allRunsLatestPatchset$.subscribe(r => (latestRuns = r));
+ model.allRunsSelectedPatchset$.subscribe(r => (selectedRuns = r));
+ const provider = createPatchsetTaggingProvider();
+ const fetchSpy = sinon.spy(provider, 'fetch');
+
+ model.register({
+ pluginName: PLUGIN_NAME,
+ provider,
+ config: CONFIG_POLLING_NONE,
+ });
+ await waitUntil(() => change === undefined);
+
+ // Viewing patchset 1 but explicitly selecting the latest patchset (2) in the
+ // checks tab. checksPatchset differs from patchNum, so it is not reset.
+ testResolver(changeViewModelToken).updateState({
+ patchNum: 1 as RevisionPatchSetNum,
+ checksPatchset: 2 as PatchSetNumber,
+ });
+ const testChange = createTwoPatchsetChange();
+ testResolver(changeModelToken).updateStateChange(testChange);
+ await waitUntil(() => deepEqual(change, testChange));
+
+ // Fire the throttled emission, then flush the fetch promise into state.
+ clock.tick(600);
+ await waitEventLoop();
+
+ // Selected patchset (2) equals latest, so still only a single fetch.
+ assert.equal(fetchSpy.callCount, 1);
+ assert.isNotEmpty(latestRuns);
+ assert.isNotEmpty(selectedRuns);
+ assert.isTrue(latestRuns.every(r => r.patchset === 2));
+ assert.isTrue(selectedRuns.every(r => r.patchset === 2));
+
+ clock.restore();
+ });
+
+ test('fetches both patchsets when an older one is selected', async () => {
+ const clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
+ let change: ParsedChangeInfo | undefined = undefined;
+ testResolver(changeModelToken).change$.subscribe(c => (change = c));
+ let latestRuns: CheckRun[] = [];
+ let selectedRuns: CheckRun[] = [];
+ model.allRunsLatestPatchset$.subscribe(r => (latestRuns = r));
+ model.allRunsSelectedPatchset$.subscribe(r => (selectedRuns = r));
+ const provider = createPatchsetTaggingProvider();
+ const fetchSpy = sinon.spy(provider, 'fetch');
+
+ model.register({
+ pluginName: PLUGIN_NAME,
+ provider,
+ config: CONFIG_POLLING_NONE,
+ });
+ await waitUntil(() => change === undefined);
+
+ // Explicitly selecting the older patchset (1); latest is 2.
+ testResolver(changeViewModelToken).updateState({
+ checksPatchset: 1 as PatchSetNumber,
+ });
+ const testChange = createTwoPatchsetChange();
+ testResolver(changeModelToken).updateStateChange(testChange);
+ await waitUntil(() => deepEqual(change, testChange));
+
+ // Fire the throttled emission, then flush the fetch promise into state.
+ clock.tick(600);
+ await waitEventLoop();
+
+ // Distinct patchsets require two fetches: one for latest, one for selected.
+ assert.equal(fetchSpy.callCount, 2);
+ // The latest tab shows patchset 2, the selected tab shows the older 1.
+ assert.isNotEmpty(latestRuns);
+ assert.isNotEmpty(selectedRuns);
+ assert.isTrue(latestRuns.every(r => r.patchset === 2));
+ assert.isTrue(selectedRuns.every(r => r.patchset === 1));
+
+ clock.restore();
+ });
+
test('fetch throttle', async () => {
const clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
let change: ParsedChangeInfo | undefined = undefined;
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index f380422..239059b 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -23,7 +23,12 @@
import {DraftInfo, FixSuggestionInfo} from '../../types/common';
import {OpenFixPreviewEventDetail} from '../../types/events';
import {isDefined} from '../../types/types';
-import {AiAgentEventDetails, Interaction} from '../../constants/reporting';
+import {
+ AiAgentChatEventDetails,
+ AiAgentCheckEventDetails,
+ AiAgentEventDetails,
+ Interaction,
+} from '../../constants/reporting';
import {ReportingService as Reporting} from '../../services/gr-reporting/gr-reporting';
import {createNew, PROVIDED_FIX_ID} from '../../utils/comment-util';
import {assert, assertIsDefined, assertNever} from '../../utils/common-util';
@@ -597,10 +602,10 @@
return hasMessage || hasMultipleLinks || hasPointers || hasFixes;
}
-function getAiAgentEventDetails(
+function getAiAgentChatEventDetails(
runResult: RunResult,
commentId?: string
-): AiAgentEventDetails | undefined {
+): AiAgentChatEventDetails | undefined {
const externalId = runResult.externalId;
if (!externalId) return;
// Use JSON.parse. We expect agentId, conversationId, turnIndex.
@@ -620,7 +625,7 @@
}
/* eslint-disable object-shorthand */
// prettier-ignore
- const eventDetails: AiAgentEventDetails = {
+ const eventDetails: AiAgentChatEventDetails = {
'agentId': agentId,
'conversationId': conversationId,
'turnIndex': Number(turnIndex),
@@ -634,6 +639,36 @@
}
}
+function getAiAgentCheckEventDetails(
+ runResult: RunResult,
+ commentId?: string
+): AiAgentCheckEventDetails | undefined {
+ if (!runResult.externalId) return;
+ if (!runResult.isAiPowered) return;
+ if (!runResult.checkName) return;
+ if (!runResult.checkDescription) return;
+
+ /* eslint-disable object-shorthand */
+ // prettier-ignore
+ const eventDetails: AiAgentCheckEventDetails = {
+ 'checkName': runResult.checkName,
+ 'checkDescription': runResult.checkDescription,
+ 'externalId': runResult.externalId,
+ 'commentId': commentId,
+ };
+ return eventDetails;
+ /* eslint-enable object-shorthand */
+}
+
+function getAiAgentEventDetails(
+ runResult: RunResult,
+ commentId?: string
+): AiAgentEventDetails | undefined {
+ const chatDetails = getAiAgentChatEventDetails(runResult, commentId);
+ if (chatDetails) return chatDetails;
+ return getAiAgentCheckEventDetails(runResult, commentId);
+}
+
/**
* Reports a "Get AI Fix" click interaction on a Code Review Agent check.
*/
diff --git a/polygerrit-ui/app/models/checks/checks-util_test.ts b/polygerrit-ui/app/models/checks/checks-util_test.ts
index 978a844..7359381 100644
--- a/polygerrit-ui/app/models/checks/checks-util_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-util_test.ts
@@ -264,6 +264,9 @@
} as unknown as ReportingService;
const runResult = {
...createRunResult(),
+ isAiPowered: true,
+ checkName: 'test-check-name',
+ checkDescription: 'test-description',
externalId: JSON.stringify({
agentId: 'test-agent',
conversationId: 'test-conv',
@@ -294,6 +297,9 @@
} as unknown as ReportingService;
const runResult = {
...createRunResult(),
+ isAiPowered: true,
+ checkName: 'test-check-name',
+ checkDescription: 'test-description',
externalId: JSON.stringify({
agentId: 'test-agent',
conversationId: 'test-conv',
@@ -321,7 +327,10 @@
const reporting = {
reportInteraction: reportInteractionStub,
} as unknown as ReportingService;
- const runResult = createRunResult();
+ const runResult = {
+ ...createRunResult(),
+ isAiPowered: true,
+ };
reportAiAgentGetAIFix(reporting, runResult);
assert.isFalse(reportInteractionStub.called);
@@ -330,29 +339,86 @@
assert.isFalse(reportInteractionStub.called);
});
- test('does not report if externalId is invalid JSON', () => {
+ test('does not report if isAiPowered is false or missing', () => {
const reporting = {
reportInteraction: reportInteractionStub,
} as unknown as ReportingService;
const runResult = {
...createRunResult(),
- externalId: 'invalid-json',
+ isAiPowered: false,
+ externalId: 'some-id',
};
reportAiAgentGetAIFix(reporting, runResult);
assert.isFalse(reportInteractionStub.called);
+
+ const runResultMissing = {
+ ...createRunResult(),
+ isAiPowered: undefined,
+ externalId: 'some-id',
+ };
+
+ reportAiAgentCommentDraft(reporting, runResultMissing);
+ assert.isFalse(reportInteractionStub.called);
});
- test('does not report if externalId is missing required fields', () => {
+ test('reports check event details if externalId is plain string (not chat JSON)', () => {
const reporting = {
reportInteraction: reportInteractionStub,
} as unknown as ReportingService;
const runResult = {
...createRunResult(),
+ isAiPowered: true,
+ checkName: 'test-check-name',
+ checkDescription: 'test-description',
+ externalId: 'plain-string-external-id',
+ };
+
+ reportAiAgentGetAIFix(reporting, runResult);
+ assert.isTrue(reportInteractionStub.calledOnce);
+ assert.deepEqual(reportInteractionStub.lastCall.args[1], {
+ checkName: 'test-check-name',
+ checkDescription: 'test-description',
+ externalId: 'plain-string-external-id',
+ commentId: undefined,
+ });
+ });
+
+ test('reports check event details if externalId is JSON but missing chat fields', () => {
+ const reporting = {
+ reportInteraction: reportInteractionStub,
+ } as unknown as ReportingService;
+ const runResult = {
+ ...createRunResult(),
+ isAiPowered: true,
+ checkName: 'test-check-name',
+ checkDescription: 'test-description',
externalId: JSON.stringify({agentId: 'test-agent'}),
};
reportAiAgentGetAIFix(reporting, runResult);
+ assert.isTrue(reportInteractionStub.calledOnce);
+ assert.deepEqual(reportInteractionStub.lastCall.args[1], {
+ checkName: 'test-check-name',
+ checkDescription: 'test-description',
+ externalId: runResult.externalId,
+ commentId: undefined,
+ });
+ });
+
+ test('does not report check event if checkDescription is missing', () => {
+ const reporting = {
+ reportInteraction: reportInteractionStub,
+ } as unknown as ReportingService;
+ const runResult = {
+ ...createRunResult(),
+ isAiPowered: true,
+ checkName: 'test-check-name',
+ checkDescription: undefined,
+ externalId: 'plain-string-external-id',
+ };
+
+ reportAiAgentGetAIFix(reporting, runResult);
assert.isFalse(reportInteractionStub.called);
});
});
diff --git a/polygerrit-ui/app/models/flows/flows-model.ts b/polygerrit-ui/app/models/flows/flows-model.ts
index 300a1b7..83d0c74 100644
--- a/polygerrit-ui/app/models/flows/flows-model.ts
+++ b/polygerrit-ui/app/models/flows/flows-model.ts
@@ -31,12 +31,26 @@
export const SUBMIT_ACTION_NAME = 'submit';
+/**
+ * Matches the base change path prefix up to the change number (e.g. `/c/project/+/123`),
+ * ignoring any trailing patchsets, diff ranges, comment IDs, or file paths.
+ *
+ * @see RoutePattern.CHANGE in `gr-router.ts` for corresponding route pattern.
+ */
+const CHANGE_PREFIX_PATTERN = /^(.*?\/(?:c\/.+?\/\+)\/\d+)/;
+
export function getSubmitCondition() {
return getChangePrefix() + ' is is:submittable';
}
+/**
+ * Returns the change URL prefix (e.g. `http://host/c/project/+/123`), stripping
+ * patchset numbers, diff ranges, comment IDs, and file paths from pathname.
+ */
export function getChangePrefix() {
- return window.location.origin + window.location.pathname;
+ const match = window.location.pathname.match(CHANGE_PREFIX_PATTERN);
+ const pathname = match ? match[1] : window.location.pathname;
+ return window.location.origin + pathname;
}
export class FlowsModel extends Model<FlowsState> {
diff --git a/polygerrit-ui/app/models/flows/flows-model_test.ts b/polygerrit-ui/app/models/flows/flows-model_test.ts
index 2dfe5a7..18c9538 100644
--- a/polygerrit-ui/app/models/flows/flows-model_test.ts
+++ b/polygerrit-ui/app/models/flows/flows-model_test.ts
@@ -200,4 +200,69 @@
await waitUntil(() => !flowsModel.hasAutosubmitFlowAlready());
assert.isFalse(flowsModel.hasAutosubmitFlowAlready());
});
+
+ suite('getChangePrefix', () => {
+ let originalPath: string;
+
+ setup(() => {
+ originalPath = window.location.pathname;
+ });
+
+ teardown(() => {
+ window.history.replaceState(null, '', originalPath);
+ });
+
+ test('strips patchset, diff range, and file path subpaths', () => {
+ const origin = window.location.origin;
+
+ window.history.replaceState(null, '', '/c/my-repo/+/123');
+ assert.equal(getChangePrefix(), `${origin}/c/my-repo/+/123`);
+
+ window.history.replaceState(null, '', '/c/my-repo/+/123/4');
+ assert.equal(getChangePrefix(), `${origin}/c/my-repo/+/123`);
+
+ window.history.replaceState(null, '', '/c/my-repo/+/123/1..4');
+ assert.equal(getChangePrefix(), `${origin}/c/my-repo/+/123`);
+
+ window.history.replaceState(
+ null,
+ '',
+ '/c/my-repo/+/123/1..4/src/file.ts'
+ );
+ assert.equal(getChangePrefix(), `${origin}/c/my-repo/+/123`);
+ });
+ });
+
+ test('hasAutosubmitFlowAlready detects flows when navigating between patchsets/diff ranges', async () => {
+ const originalPath = window.location.pathname;
+ try {
+ window.history.replaceState(null, '', '/c/my-repo/+/123/4..5/src/foo.ts');
+ stubRestApi('getIfFlowsIsEnabled').resolves({enabled: true});
+ const expectedPrefix = `${window.location.origin}/c/my-repo/+/123`;
+ stubRestApi('listFlows').resolves([
+ createFlow({
+ uuid: 'flow1',
+ stages: [
+ {
+ expression: {
+ condition: `${expectedPrefix} is is:submittable`,
+ action: {name: SUBMIT_ACTION_NAME},
+ },
+ state: FlowStageState.DONE,
+ },
+ ],
+ }),
+ ]);
+
+ changeModel.updateStateChange({
+ ...createParsedChange(),
+ _number: 123 as NumericChangeId,
+ });
+ await waitUntil(() => flowsModel.getState().flows.length > 0);
+
+ assert.isTrue(flowsModel.hasAutosubmitFlowAlready());
+ } finally {
+ window.history.replaceState(null, '', originalPath);
+ }
+ });
});
diff --git a/polygerrit-ui/app/models/plugins/plugins-model.ts b/polygerrit-ui/app/models/plugins/plugins-model.ts
index 8ed7cfe..9074c21 100644
--- a/polygerrit-ui/app/models/plugins/plugins-model.ts
+++ b/polygerrit-ui/app/models/plugins/plugins-model.ts
@@ -12,7 +12,11 @@
} from '../../api/checks';
import {Model} from '../base/model';
import {select} from '../../utils/observable-util';
-import {CoverageProvider, TokenHoverListener} from '../../api/annotation';
+import {
+ CoverageProvider,
+ DiffLayerFactory,
+ TokenHoverListener,
+} from '../../api/annotation';
import {SuggestionsProvider} from '../../api/suggestions';
import {ChangeUpdatesPublisher} from '../../api/change-updates';
import {AiCodeReviewProvider} from '../../api/ai-code-review';
@@ -59,6 +63,11 @@
listener: TokenHoverListener;
}
+export interface DiffLayerPlugin {
+ pluginName: string;
+ factory: DiffLayerFactory;
+}
+
export interface ChecksUpdate {
pluginName: string;
run: CheckRun;
@@ -111,6 +120,11 @@
* annotationApi().addTokenHoverListener().
*/
tokenHighlightPlugins: TokenHoverListenerPlugin[];
+
+ /**
+ * List of plugins that have registered a diff layer factory.
+ */
+ diffLayerPlugins: DiffLayerPlugin[];
}
export class PluginsModel extends Model<PluginsState> {
@@ -154,6 +168,11 @@
state => state.suggestionsPlugins
);
+ public diffLayerPlugins$ = select(
+ this.state$,
+ state => state.diffLayerPlugins
+ );
+
public pluginsLoaded$ = select(this.state$, state => state.pluginsLoaded);
constructor() {
@@ -167,6 +186,7 @@
flowsAutosubmitPlugins: [],
suggestionsPlugins: [],
tokenHighlightPlugins: [],
+ diffLayerPlugins: [],
});
}
@@ -302,6 +322,22 @@
this.setState(nextState);
}
+ diffLayerRegister(plugin: DiffLayerPlugin) {
+ const nextState = {...this.getState()};
+ nextState.diffLayerPlugins = [...nextState.diffLayerPlugins];
+ const alreadyRegistered = nextState.diffLayerPlugins.some(
+ p => p.pluginName === plugin.pluginName
+ );
+ if (alreadyRegistered) {
+ console.warn(
+ `${plugin.pluginName} tried to register twice as a diff layer provider. Ignored.`
+ );
+ return;
+ }
+ nextState.diffLayerPlugins.push(plugin);
+ this.setState(nextState);
+ }
+
checksUpdate(update: ChecksUpdate) {
const plugins = this.getState().checksPlugins;
const plugin = plugins.find(p => p.pluginName === update.pluginName);
diff --git a/polygerrit-ui/app/models/plugins/plugins-model_test.ts b/polygerrit-ui/app/models/plugins/plugins-model_test.ts
index 7aabae7..bdcb8b1 100644
--- a/polygerrit-ui/app/models/plugins/plugins-model_test.ts
+++ b/polygerrit-ui/app/models/plugins/plugins-model_test.ts
@@ -4,9 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../test/common-test-setup';
+import * as sinon from 'sinon';
import './plugins-model';
import {ChecksApiConfig, ChecksProvider, ResponseCode} from '../../api/checks';
-import {ChecksPlugin, ChecksUpdate, PluginsModel} from './plugins-model';
+import {
+ ChecksPlugin,
+ ChecksUpdate,
+ DiffLayerPlugin,
+ PluginsModel,
+} from './plugins-model';
import {createRun, createRunResult} from '../../test/test-data-generators';
import {assert} from '@open-wc/testing';
@@ -29,6 +35,7 @@
suite('plugins-model tests', () => {
let model: PluginsModel;
let checksPlugins: ChecksPlugin[] = [];
+ let diffLayerPlugins: DiffLayerPlugin[] = [];
const register = function () {
model.checksRegister({
pluginName: PLUGIN_NAME,
@@ -41,6 +48,7 @@
model = new PluginsModel();
model.state$.subscribe(s => {
checksPlugins = s.checksPlugins;
+ diffLayerPlugins = s.diffLayerPlugins;
});
});
@@ -81,4 +89,33 @@
assert.equal(update?.pluginName, PLUGIN_NAME);
});
+
+ test('diffLayerRegister', async () => {
+ assert.isFalse(diffLayerPlugins.some(p => p.pluginName === PLUGIN_NAME));
+
+ const factory = () => {
+ return {
+ annotate: () => {},
+ };
+ };
+ model.diffLayerRegister({
+ pluginName: PLUGIN_NAME,
+ factory,
+ });
+
+ assert.isTrue(diffLayerPlugins.some(p => p.pluginName === PLUGIN_NAME));
+ assert.equal(
+ diffLayerPlugins.find(p => p.pluginName === PLUGIN_NAME)?.factory,
+ factory
+ );
+
+ // Try to register again
+ const consoleWarnStub = sinon.stub(console, 'warn');
+ model.diffLayerRegister({
+ pluginName: PLUGIN_NAME,
+ factory,
+ });
+ assert.isTrue(consoleWarnStub.calledOnce);
+ consoleWarnStub.restore();
+ });
});
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index 03c944f..0ee5e99 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -253,6 +253,9 @@
) {
params.push(`checksPatchset=${state.checksPatchset}`);
}
+ if (state.forceReload) {
+ params.push('forceReload=true');
+ }
if (params.length > 0) {
queryParams = '?' + params.join('&');
}
@@ -285,6 +288,26 @@
return `${createChangeUrlCommon(state)}${path},edit${suffix}`;
}
+export function createApplyFixUrl(
+ obj: (CreateChangeUrlObject | Omit<ChangeViewState, 'view' | 'childView'>) & {
+ filePath?: string;
+ currentChildView?: ChangeChildView;
+ }
+): string {
+ const {filePath, currentChildView, ...restObj} = obj;
+ if (currentChildView === ChangeChildView.DIFF && filePath) {
+ return createDiffUrl({
+ ...restObj,
+ patchNum: EDIT,
+ diffView: {path: filePath},
+ });
+ }
+ return createChangeUrl({
+ ...restObj,
+ patchNum: EDIT,
+ });
+}
+
/**
* The shared part of creating a change URL between OVERVIEW, DIFF and EDIT
* child views.
diff --git a/polygerrit-ui/app/models/views/change_test.ts b/polygerrit-ui/app/models/views/change_test.ts
index e05adb6..8bebe21 100644
--- a/polygerrit-ui/app/models/views/change_test.ts
+++ b/polygerrit-ui/app/models/views/change_test.ts
@@ -6,10 +6,13 @@
import {assert} from '@open-wc/testing';
import {
BasePatchSetNum,
+ EDIT,
+ NumericChangeId,
PatchSetNumber,
RepoName,
RevisionPatchSetNum,
} from '../../api/rest-api';
+
import '../../test/common-test-setup';
import {
createChangeViewState,
@@ -17,7 +20,9 @@
createEditViewState,
} from '../../test/test-data-generators';
import {
+ ChangeChildView,
ChangeViewState,
+ createApplyFixUrl,
createChangeUrl,
createDiffUrl,
createEditUrl,
@@ -114,6 +119,14 @@
);
});
+ test('forceReload', () => {
+ params.forceReload = true;
+ assert.equal(
+ createDiffUrl(params),
+ '/c/test-project/+/42/12/x%252By/path.cpp?forceReload=true'
+ );
+ });
+
test('base patchset', () => {
params.basePatchNum = 6 as BasePatchSetNum;
assert.equal(
@@ -186,4 +199,36 @@
assert.equal(createEditUrl(params).substring(0, 5), '/base');
window.CANONICAL_PATH = undefined;
});
+
+ suite('createApplyFixUrl', () => {
+ test('Diff View context', () => {
+ assert.equal(
+ createApplyFixUrl({
+ changeNum: 42 as NumericChangeId,
+ repo: 'test-project' as RepoName,
+ patchNum: EDIT,
+ basePatchNum: 1 as BasePatchSetNum,
+ forceReload: true,
+ filePath: 'foo/bar.ts',
+ currentChildView: ChangeChildView.DIFF,
+ }),
+ '/c/test-project/+/42/1..edit/foo/bar.ts?forceReload=true'
+ );
+ });
+
+ test('Overview context', () => {
+ assert.equal(
+ createApplyFixUrl({
+ changeNum: 42 as NumericChangeId,
+ repo: 'test-project' as RepoName,
+ patchNum: EDIT,
+ basePatchNum: 1 as BasePatchSetNum,
+ forceReload: true,
+ filePath: 'foo/bar.ts',
+ currentChildView: ChangeChildView.OVERVIEW,
+ }),
+ '/c/test-project/+/42/1..edit?forceReload=true'
+ );
+ });
+ });
});
diff --git a/polygerrit-ui/app/pnpm-lock.yaml b/polygerrit-ui/app/pnpm-lock.yaml
index da97189..6d865aa 100644
--- a/polygerrit-ui/app/pnpm-lock.yaml
+++ b/polygerrit-ui/app/pnpm-lock.yaml
@@ -138,15 +138,15 @@
engines: {node: '>=12.0.0'}
highlightjs-closure-templates@https://codeload.github.com/highlightjs/highlightjs-closure-templates/tar.gz/02fb0646e0499084f96a99b8c6f4a0d7bd1d33ba:
- resolution: {tarball: https://codeload.github.com/highlightjs/highlightjs-closure-templates/tar.gz/02fb0646e0499084f96a99b8c6f4a0d7bd1d33ba}
+ resolution: {gitHosted: true, tarball: https://codeload.github.com/highlightjs/highlightjs-closure-templates/tar.gz/02fb0646e0499084f96a99b8c6f4a0d7bd1d33ba}
version: 0.0.1
highlightjs-epp@https://codeload.github.com/highlightjs/highlightjs-epp/tar.gz/9f9e1a92f37c217c68899c7d3bdccb4d134681b9:
- resolution: {tarball: https://codeload.github.com/highlightjs/highlightjs-epp/tar.gz/9f9e1a92f37c217c68899c7d3bdccb4d134681b9}
+ resolution: {gitHosted: true, tarball: https://codeload.github.com/highlightjs/highlightjs-epp/tar.gz/9f9e1a92f37c217c68899c7d3bdccb4d134681b9}
version: 0.0.1
highlightjs-structured-text@https://codeload.github.com/highlightjs/highlightjs-structured-text/tar.gz/e68dd7aa829529fb6c40d6287585f43273605a9e:
- resolution: {tarball: https://codeload.github.com/highlightjs/highlightjs-structured-text/tar.gz/e68dd7aa829529fb6c40d6287585f43273605a9e}
+ resolution: {gitHosted: true, tarball: https://codeload.github.com/highlightjs/highlightjs-structured-text/tar.gz/e68dd7aa829529fb6c40d6287585f43273605a9e}
version: 1.4.9
highlightjs-ttcn3@git+https://gitea.osmocom.org/ttcn3/highlightjs-ttcn3.git#6daccff309fca1e7561a43984d42fa4f829ce06d:
@@ -154,7 +154,7 @@
version: 0.0.1
highlightjs-vue@https://codeload.github.com/paladox/highlightjs-vue/tar.gz/44eed074ea0110d1ad03d2cbd77d27027cf7bb04:
- resolution: {tarball: https://codeload.github.com/paladox/highlightjs-vue/tar.gz/44eed074ea0110d1ad03d2cbd77d27027cf7bb04}
+ resolution: {gitHosted: true, tarball: https://codeload.github.com/paladox/highlightjs-vue/tar.gz/44eed074ea0110d1ad03d2cbd77d27027cf7bb04}
version: 1.1.0
ieee754@1.2.1:
@@ -224,7 +224,7 @@
engines: {node: '>= 6'}
resemblejs@https://codeload.github.com/rsmbl/Resemble.js/tar.gz/66a55c5bfc3bda2303ad632ee8ce3c727b415917:
- resolution: {tarball: https://codeload.github.com/rsmbl/Resemble.js/tar.gz/66a55c5bfc3bda2303ad632ee8ce3c727b415917}
+ resolution: {gitHosted: true, tarball: https://codeload.github.com/rsmbl/Resemble.js/tar.gz/66a55c5bfc3bda2303ad632ee8ce3c727b415917}
version: 5.0.0
rxjs@6.6.7:
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 92a9c02..51edcfc 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -2620,6 +2620,7 @@
}),
url: `${url}/fix:preview`,
anonymizedUrl: `${ANONYMIZED_REVISION_BASE_URL}/fix:preview`,
+ useReadScheduler: true,
})) as FilePathToDiffInfoMap | undefined;
if (response === undefined) {
diff --git a/polygerrit-ui/app/tsconfig_bazel.json b/polygerrit-ui/app/tsconfig_bazel.json
index 730fc4d..8cd5dfb 100644
--- a/polygerrit-ui/app/tsconfig_bazel.json
+++ b/polygerrit-ui/app/tsconfig_bazel.json
@@ -1,6 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
+ // Override the yarn/IDE outDir from tsconfig.json: rules_ts requires the
+ // output directory to stay within the Bazel package (kept in sync with
+ // out_dir of the compile_pg ts_project in the BUILD file).
+ "outDir": "_pg_ts_out",
"typeRoots": [
"../../external/ui_npm/node_modules/@types",
"../../external/ui_dev_npm/node_modules/@types"
diff --git a/polygerrit-ui/app/tsconfig_bazel_test.json b/polygerrit-ui/app/tsconfig_bazel_test.json
index 7d74ffa..e6fc2cc 100644
--- a/polygerrit-ui/app/tsconfig_bazel_test.json
+++ b/polygerrit-ui/app/tsconfig_bazel_test.json
@@ -1,6 +1,10 @@
{
"extends": "./tsconfig_bazel.json",
"compilerOptions": {
+ // Override the yarn/IDE outDir from tsconfig.json: rules_ts requires the
+ // output directory to stay within the Bazel package (kept in sync with
+ // out_dir of the compile_pg_with_tests ts_project).
+ "outDir": "_pg_with_tests_out",
"typeRoots": [
"node_modules/@types",
"../node_modules/@types"
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 52a4794..2a2ec16 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -110,7 +110,6 @@
export type ChangeEvent = InputEvent;
-// TODO: This event seems to be unused (no listener). Remove?
export type ChangedEvent = CustomEvent<string | undefined>;
export interface ChangeMessageDeletedEventDetail {
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 4b0c1ab..ef08cf0 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -96,15 +96,6 @@
const path1 = c1.path ?? '';
const path2 = c2.path ?? '';
if (path1 !== path2) {
- // TODO: Why is this logic not part of specialFilePathCompare()?
- // '/PATCHSET' will not come before '/COMMIT' when sorting
- // alphabetically so move it to the front explicitly
- if (path1 === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
- return -1;
- }
- if (path2 === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
- return 1;
- }
return specialFilePathCompare(path1, path2);
}
@@ -211,7 +202,7 @@
path: replyingTo.path,
patch_set: replyingTo.patch_set,
side: replyingTo.side,
- line: replyingTo.line,
+ line: replyingTo.range ? replyingTo.range.end_line : replyingTo.line,
range: replyingTo.range,
parent: replyingTo.parent,
in_reply_to: replyingTo.id,
diff --git a/polygerrit-ui/app/utils/comment-util_test.ts b/polygerrit-ui/app/utils/comment-util_test.ts
index e8fe606..abda42c 100644
--- a/polygerrit-ui/app/utils/comment-util_test.ts
+++ b/polygerrit-ui/app/utils/comment-util_test.ts
@@ -8,6 +8,7 @@
computeDisplayLine,
createCommentThreads,
createNew,
+ createNewReply,
createUserFixSuggestion,
getContentInCommentRange,
getMentionedThreads,
@@ -748,4 +749,36 @@
assert.equal(computeDisplayLine({}), '');
});
});
+
+ suite('createNewReply', () => {
+ test('standard comment reply (no range)', () => {
+ const replyingTo = {
+ ...createComment(),
+ id: 'parent_id' as UrlEncodedCommentId,
+ line: 5,
+ };
+ const reply = createNewReply(replyingTo, 'reply message', false);
+ assert.equal(reply.line, 5);
+ assert.isUndefined(reply.range);
+ assert.equal(reply.in_reply_to, 'parent_id');
+ });
+
+ test('reply to comment with range (mismatched line)', () => {
+ const replyingTo = {
+ ...createComment(),
+ id: 'parent_id' as UrlEncodedCommentId,
+ line: 1,
+ range: {
+ start_line: 1,
+ start_character: 0,
+ end_line: 2,
+ end_character: 5,
+ },
+ };
+ const reply = createNewReply(replyingTo, 'reply message', false);
+ assert.equal(reply.line, 2); // Should match range.end_line, not replyingTo.line
+ assert.deepEqual(reply.range, replyingTo.range);
+ assert.equal(reply.in_reply_to, 'parent_id');
+ });
+ });
});
diff --git a/polygerrit-ui/app/utils/path-list-util.ts b/polygerrit-ui/app/utils/path-list-util.ts
index 5c523b3..31e17c9 100644
--- a/polygerrit-ui/app/utils/path-list-util.ts
+++ b/polygerrit-ui/app/utils/path-list-util.ts
@@ -8,6 +8,14 @@
import {hasOwnProperty} from './common-util';
export function specialFilePathCompare(a: string, b: string) {
+ // Patchset-level comments always go first.
+ if (a === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+ return -1;
+ }
+ if (b === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+ return 1;
+ }
+
// The commit message always goes first.
if (a === SpecialFilePath.COMMIT_MESSAGE) {
return -1;
diff --git a/polygerrit-ui/app/utils/path-list-util_test.ts b/polygerrit-ui/app/utils/path-list-util_test.ts
index cdd8182..a7c1e53 100644
--- a/polygerrit-ui/app/utils/path-list-util_test.ts
+++ b/polygerrit-ui/app/utils/path-list-util_test.ts
@@ -21,12 +21,14 @@
const testFiles = [
'/a.h',
'/MERGE_LIST',
+ SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
'/a.cpp',
'/COMMIT_MSG',
'/asdasd',
'/mrPeanutbutter.py',
];
assert.deepEqual(testFiles.sort(specialFilePathCompare), [
+ SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
'/COMMIT_MSG',
'/MERGE_LIST',
'/a.h',
diff --git a/polygerrit-ui/polygerrit.MODULE.bazel b/polygerrit-ui/polygerrit.MODULE.bazel
index d839cc8..e04ccd6 100644
--- a/polygerrit-ui/polygerrit.MODULE.bazel
+++ b/polygerrit-ui/polygerrit.MODULE.bazel
@@ -27,34 +27,12 @@
)
use_repo(rules_ts_ext, "npm_typescript")
-NODE_24_REPO = {
- "24.14.1-darwin_arm64": ("node-v24.14.1-darwin-arm64.tar.gz", "node-v24.14.1-darwin-arm64", "25495ff85bd89e2d8a24d88566d7e2f827c6b0d3d872b2cebf75371f93fcb1fe"),
- "24.14.1-darwin_amd64": ("node-v24.14.1-darwin-x64.tar.gz", "node-v24.14.1-darwin-x64", "2526230ad7d922be82d4fdb1e7ee1e84303e133e3b4b0ec4c2897ab31de0253d"),
- "24.14.1-linux_arm64": ("node-v24.14.1-linux-arm64.tar.xz", "node-v24.14.1-linux-arm64", "71e427e28b78846f201d4d5ecc30cb13d1508ca099ef3871889a1256c7d6f67e"),
- "24.14.1-linux_ppc64le": ("node-v24.14.1-linux-ppc64le.tar.xz", "node-v24.14.1-linux-ppc64le", "95bf0c8dbb73144edb79a57399f03c70af6995b78e1c632926e53e6404662ef5"),
- "24.14.1-linux_s390x": ("node-v24.14.1-linux-s390x.tar.xz", "node-v24.14.1-linux-s390x", "ed3bfbc0ff418b0ec4633f23d53a12a691717a34b041c3fbdb296c8774e5a98a"),
- "24.14.1-linux_amd64": ("node-v24.14.1-linux-x64.tar.xz", "node-v24.14.1-linux-x64", "84d38715d449447117d05c3e71acd78daa49d5b1bfa8aacf610303920c3322be"),
- "24.14.1-windows_amd64": ("node-v24.14.1-win-x64.zip", "node-v24.14.1-win-x64", "6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70"),
-}
-
+# The node version is read from //:.nvmrc, the cross-tool convention file
+# also honored by nvm and CI -- Bazel and the yarn dev flow share one pin.
+# Versions unknown to the pinned rules_nodejs release are auto-fetched and
+# recorded in MODULE.bazel.lock (rules_nodejs >= 6.7.5).
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
-node.toolchain(
- name = "nodejs",
- node_repositories = NODE_24_REPO,
- node_version = "24.14.1",
-)
-use_repo(node, "nodejs_toolchains")
-use_repo(node, "nodejs_darwin_arm64")
-use_repo(node, "nodejs_darwin_amd64")
-use_repo(node, "nodejs_linux_arm64")
-use_repo(node, "nodejs_linux_ppc64le")
-use_repo(node, "nodejs_linux_s390x")
-use_repo(node, "nodejs_linux_amd64")
-use_repo(node, "nodejs_windows_amd64")
-
-pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
-pnpm.pnpm(pnpm_version = "10.33.3")
-use_repo(pnpm, "pnpm")
+node.toolchain(node_version_from_nvmrc = "//:.nvmrc")
npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
npm.npm_translate_lock(
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-ai-review-prompt-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-ai-review-prompt-dark.png
index 7bbdc5b..436031d 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-ai-review-prompt-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-ai-review-prompt-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-ai-review-prompt.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-ai-review-prompt.png
index ff592ae..9aca5a8 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-ai-review-prompt.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-ai-review-prompt.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips-dark.png
index 782a764..1cd5d8d 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips.png
index 1368d3d..569b3bb 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-summary-with-chips.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-left-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-left-dark.png
new file mode 100644
index 0000000..0f4857e
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-left-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-left.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-left.png
new file mode 100644
index 0000000..2464d3f
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-left.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-right-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-right-dark.png
new file mode 100644
index 0000000..238c3fb
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-right-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-right.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-right.png
new file mode 100644
index 0000000..ff32fb3
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-content-with-sidebar-right.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-cancel-modal-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-cancel-modal-dark.png
new file mode 100644
index 0000000..fe699cb
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-cancel-modal-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-cancel-modal.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-cancel-modal.png
new file mode 100644
index 0000000..d6df16b
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-cancel-modal.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-normal-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-normal-dark.png
new file mode 100644
index 0000000..dc9928d
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-normal-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-normal.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-normal.png
new file mode 100644
index 0000000..c4b94d4
--- /dev/null
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-editor-view-normal.png
Binary files differ
diff --git a/polygerrit-ui/web-test-runner.config.mjs b/polygerrit-ui/web-test-runner.config.mjs
index 2a6f695..804fd9a 100644
--- a/polygerrit-ui/web-test-runner.config.mjs
+++ b/polygerrit-ui/web-test-runner.config.mjs
@@ -9,7 +9,9 @@
import { playwrightLauncher } from '@web/test-runner-playwright';
const runUnderBazel = !!process.env['RUNFILES_DIR'];
-const diffThreshold = 0.01;
+// We set this to a non-zero value because of sub-pixel rendering noise that
+// can create false positives.
+const diffThreshold = .02;
function testRunnerHtmlFactory(prefix) {
return (testFramework) => `
@@ -36,11 +38,11 @@
];
}
- const runfilesRoot = path.dirname(process.cwd());
+ const cwd = process.cwd();
return [
- path.join(runfilesRoot, 'plugins_npm', 'node_modules'),
- path.join(runfilesRoot, 'ui_npm', 'node_modules'),
- path.join(runfilesRoot, 'ui_dev_npm', 'node_modules'),
+ path.join(cwd, 'plugins/node_modules'),
+ path.join(cwd, 'polygerrit-ui/node_modules'),
+ path.join(cwd, 'polygerrit-ui/app/node_modules'),
];
}
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 17e0fcc..06f07b5 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -87,7 +87,9 @@
{else}
<link rel="icon" type="image/x-icon" href="{$canonicalPath}/favicon.ico">{\n}
{/if}
- <link rel="manifest" href="{$manifestPath}">{\n}
+ {if $manifestPath}
+ <link rel="manifest" href="{$manifestPath}">{\n}
+ {/if}
{if $changeRequestsPath}
{if $defaultChangeDetailHex}
<link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/detail?O={$defaultChangeDetailHex}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
diff --git a/resources/com/google/gerrit/server/mail/RegisterNewEmailHtml.soy b/resources/com/google/gerrit/server/mail/RegisterNewEmailHtml.soy
index 20f9999..a485abd 100644
--- a/resources/com/google/gerrit/server/mail/RegisterNewEmailHtml.soy
+++ b/resources/com/google/gerrit/server/mail/RegisterNewEmailHtml.soy
@@ -31,7 +31,7 @@
<p>
- {$email.emailRegistrationLink}
+ <a href="{$email.emailRegistrationLink}">{$email.emailRegistrationLink}</a>
</p>
<p>
If you have received this mail in error, you do not need to take any
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index 66f21b9..f557cfd 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -244,6 +244,7 @@
svg = application/xml
svh = text/x-systemverilog
swift = text/x-swift
+tada = text/x-python
tcl = text/x-tcl
tex = text/x-latex
text = text/plain
diff --git a/tools/bazlets.MODULE.bazel b/tools/bazlets.MODULE.bazel
index 49e736d..8bf405d 100644
--- a/tools/bazlets.MODULE.bazel
+++ b/tools/bazlets.MODULE.bazel
@@ -6,7 +6,7 @@
bazel_dep(name = "com_googlesource_gerrit_bazlets")
git_override(
module_name = "com_googlesource_gerrit_bazlets",
- commit = "06126c5e41d7a94f2b6d11af8e7e97a490b6e310",
+ commit = "2366eccc72aac1a966ef3446c98a80230a7b5d89",
remote = "https://gerrit.googlesource.com/bazlets",
)
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index ebc2c1e..e99384b 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -71,6 +71,9 @@
"libgerrit-prolog-common",
"libjgit-archive",
"libjgit-servlet",
+ # Core WARs do not package JGit LFS server jars today. If that changes,
+ # exclude both the canonical and EE8 LFS jars here as Gerrit-internal jars.
+ "libjgit-servlet-ee8",
"libquery_parser",
"libssh-apache",
"log4j-config",
diff --git a/tools/deps.toml b/tools/deps.toml
index 6444daf..e500cd6 100644
--- a/tools/deps.toml
+++ b/tools/deps.toml
@@ -2,13 +2,13 @@
antlr = "3.5.2"
autoValueGson = "1.3.1"
bouncyCastle = "1.84"
-byteBuddy = "1.18.10"
+byteBuddy = "1.18.11"
caffeine = "2.9.2"
commonmark = "0.24.0"
gitiles = "1.6.0"
greenmail = "1.5.5"
httpcomp = "4.5.14"
-jetty = "12.1.10"
+jetty = "12.1.11"
mail = "1.6.0"
mime4j = "0.8.1"
ow2 = "9.9.1"
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 12567ba..6bd0763 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -200,8 +200,6 @@
classpathentry('src', 'modules/jgit/org.eclipse.jgit.archive/src',
excluding='org/eclipse/jgit/archive/FormatActivator.java')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.archive/resources')
- classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/src')
- classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/resources')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.junit/src')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.apache/src')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.apache/resources')
@@ -261,6 +259,21 @@
for p in source_cp:
source_by_basename[_normalize_jar_basename(p)] = p
+ generated_jgit_sources = {
+ "org.eclipse.jgit.http.server.ee8/libjgit-servlet-ee8.jar":
+ "org.eclipse.jgit.http.server.ee8/jgit-http-server-ee8-srcs.srcjar",
+ "org.eclipse.jgit.lfs.server.ee8/libjgit-lfs-server-ee8.jar":
+ "org.eclipse.jgit.lfs.server.ee8/jgit-lfs-server-ee8-srcs.srcjar",
+ }
+
+ def generated_jgit_source(jar):
+ for jar_suffix, src_suffix in generated_jgit_sources.items():
+ if jar.endswith(jar_suffix):
+ candidate = jar[:-len(jar_suffix)] + src_suffix
+ if os.path.exists(candidate):
+ return candidate
+ return None
+
for p in runtime_cp:
if p.endswith('-src.jar'):
continue
@@ -339,12 +352,12 @@
if os.path.basename(j) == "libjava-prettify.jar" and "/external/" in j:
continue
- s = None
+ s = generated_jgit_source(j)
# Attach sources using the classpath_collector output from rules_jvm_external.
# This replaces the previous heuristic-based source lookup.
key = _normalize_jar_basename(j)
- if key in source_by_basename:
+ if not s and key in source_by_basename:
sp = _resolve_repo_path(output_base, source_by_basename[key])
s = sp
@@ -441,4 +454,3 @@
except KeyboardInterrupt:
print('Interrupted by user', file=sys.stderr)
exit(1)
-
diff --git a/tools/java_deps.MODULE.bazel b/tools/java_deps.MODULE.bazel
index a899132..378b378 100644
--- a/tools/java_deps.MODULE.bazel
+++ b/tools/java_deps.MODULE.bazel
@@ -137,6 +137,38 @@
for coord in _GERRIT_FORCED_ARTIFACTS
]
+# We consume Guice's unshaded artifact (published as the "classes"
+# classifier) plus an external OW2 ASM (see //lib/guice and tools/nongoogle.toml),
+# because the ASM shaded into the default guice jar cannot read Java 25 class
+# files and corrupts Guice's error-message source formatting. The external ASM
+# must be >= 9.8, the first release with Java 25 (class major 69) support;
+# Gerrit pins 9.9.1.
+#
+# That only holds if the plain (fat) com.google.inject:guice never reaches the
+# classpath. These artifacts pull it in transitively, so strip that edge from
+# each; they resolve Guice solely from the //lib/guice classes-classifier jar.
+# Keep this list in sync with everything that depends on Guice transitively --
+# a missed entry silently reintroduces the shaded ASM.
+#
+# TODO(davido): Drop these exclusions (and the classes classifier) once Guice
+# ships a default jar with an updated shaded ASM that reads current class
+# files: https://github.com/google/guice/issues/1926
+_GUICE_FAT_JAR_CONSUMERS = [
+ "com.google.inject.extensions:guice-assistedinject",
+ "com.google.inject.extensions:guice-servlet",
+ "com.google.template:soy",
+ "org.openid4java:openid4java",
+]
+
+[
+ maven.amend_artifact(
+ name = "external_deps",
+ coordinates = coord,
+ exclusions = ["com.google.inject:guice"],
+ )
+ for coord in _GUICE_FAT_JAR_CONSUMERS
+]
+
# Empty maven.install call to apply the configuration to the installation from
# toml files.
maven.install(
diff --git a/tools/jgit-ee8/BUILD b/tools/jgit-ee8/BUILD
new file mode 100644
index 0000000..fa80f5c
--- /dev/null
+++ b/tools/jgit-ee8/BUILD
@@ -0,0 +1,21 @@
+load("@rules_shell//shell:sh_test.bzl", "sh_test")
+
+# Guard: //:headless must not depend on the canonical Jakarta JGit
+# servlet jar. //lib:jgit-servlet exports the EE8 bridge; both jars
+# share class FQNs and cannot co-exist on a classpath.
+
+genquery(
+ name = "original_jgit_servlet_path",
+ expression = "somepath(//:headless, @jgit//org.eclipse.jgit.http.server:jgit-servlet)",
+ scope = [
+ "//:headless",
+ "@jgit//org.eclipse.jgit.http.server:jgit-servlet",
+ ],
+)
+
+sh_test(
+ name = "classpath_duplicates_test",
+ srcs = ["classpath_duplicates_test.sh"],
+ args = ["$(location :original_jgit_servlet_path)"],
+ data = [":original_jgit_servlet_path"],
+)
diff --git a/tools/jgit-ee8/classpath_duplicates_test.sh b/tools/jgit-ee8/classpath_duplicates_test.sh
new file mode 100755
index 0000000..8c891a3
--- /dev/null
+++ b/tools/jgit-ee8/classpath_duplicates_test.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+#
+# 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.
+
+set -euo pipefail
+
+for query_output in "$@"; do
+ if [[ -s "${query_output}" ]]; then
+ echo "Unexpected path to original JGit servlet-facing target:" >&2
+ cat "${query_output}" >&2
+ exit 1
+ fi
+done
diff --git a/tools/nongoogle.toml b/tools/nongoogle.toml
index caecf6c..1f2e24a 100644
--- a/tools/nongoogle.toml
+++ b/tools/nongoogle.toml
@@ -3,8 +3,8 @@
[versions]
autoCommon = "1.2.2"
-autoFactory = "1.0.1"
-autoValue= "1.11.0"
+autoFactory = "1.1.0"
+autoValue= "1.11.1"
flogger = "0.8"
# Also update the GUAVA version in java/com/google/gerrit/extensions/BUILD
# to keep Bazel and extension dependencies in sync.
@@ -12,7 +12,7 @@
guice = "6.0.0"
lucene = "10.4.0"
slf4j = "2.0.18"
-sshd = "2.18.0"
+sshd = "2.19.0"
truth = "1.4.4"
[libraries]
@@ -32,7 +32,7 @@
guava = { module = "com.google.guava:guava", version.ref = "guava" }
guava-testlib = { module = "com.google.guava:guava-testlib", version.ref = "guava" }
guice-assistedinject = { module = "com.google.inject.extensions:guice-assistedinject", version.ref = "guice" }
-guice-library = { module = "com.google.inject:guice", version.ref = "guice" }
+guice-library = { module = "com.google.inject:guice", version.ref = "guice", classifier = "classes" }
guice-servlet = { module = "com.google.inject.extensions:guice-servlet", version.ref = "guice" }
h2 = { module = "com.h2database:h2", version = "2.4.240" }
hamcrest = { module = "org.hamcrest:hamcrest", version = "3.0" }
@@ -49,7 +49,7 @@
lucene-core = { module = "org.apache.lucene:lucene-core", version.ref = "lucene" }
lucene-misc = { module = "org.apache.lucene:lucene-misc", version.ref = "lucene" }
lucene-queryparser = { module = "org.apache.lucene:lucene-queryparser", version.ref = "lucene" }
-mina-core = { module = "org.apache.mina:mina-core", version = "2.2.4" }
+mina-core = { module = "org.apache.mina:mina-core", version = "2.2.9" }
nekohtml = { module = "net.sourceforge.nekohtml:nekohtml", version = "1.9.10" }
openid-consumer = { module = "org.openid4java:openid4java", version = "1.0.0" }
protobuf-java = { module = "com.google.protobuf:protobuf-java", version = "4.35.0" }