Merge "Revert "Fail 'Get Diff' requests for file sizes that exceed 50Mb""
diff --git a/.mailmap b/.mailmap
deleted file mode 100644
index 721f3c0..0000000
--- a/.mailmap
+++ /dev/null
@@ -1,97 +0,0 @@
-Adrian Görler <adrian.goerler@sap.com> Adrian Goerler <adrian.goerler@sap.com>
-Ahaan Ugale <ahaanugale@gmail.com> <augale@codeaurora.org>
-Alex Blewitt <alex.blewitt@gmail.com> <alex.blewitt@gs.com>
-Alex Blewitt <alex.blewitt@gmail.com> <alex.blewitt@credit-suisse.com>
-Alex Ryazantsev <alex.ryazantsev@gmail.com> alex <alex.ryazantsev@gmail.com>
-Alex Ryazantsev <alex.ryazantsev@gmail.com> alex.ryazantsev <alex.ryazantsev@gmail.com>
-Alice Kober-Sotzek <aliceks@google.com> <aliceks@google.com>
-Alexandre Philbert <alexandre.philbert@ericsson.com> <alexandre.philbert@hotmail.com>
-Andrew Bonventre <andybons@chromium.org> <andybons@google.com>
-Becky Siegel <beckysiegel@google.com> beckysiegel <beckysiegel@google.com>
-Ben Rohlfs <brohlfs@google.com> brohlfs <brohlfs@google.com>
-Brad Larson <bklarson@gmail.com> <brad.larson@garmin.com>
-Bruce Zu <bruce.zu.run10@gmail.com> <bruce.zu@sonyericsson.com>
-Bruce Zu <bruce.zu.run10@gmail.com> <bruce.zu@sonymobile.com>
-Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com> carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
-Chad Horohoe <chorohoe@wikimedia.org> <chadh@wikimedia.org>
-Changcheng Xiao <xchangcheng@google.com> xchangcheng
-Cheng Ke <chengke.info@gmail.com> <chengke.info@gmail.com>
-Dariusz Luksza <dluksza@collab.net> <dariusz@luksza.org>
-Darrien Glasser <darrien@arista.com> darrien <darrien@arista.com>
-Dave Borowitz <dborowitz@google.com> <dborowitz@google.com>
-David Ostrovsky <david@ostrovsky.org> <d.ostrovsky@gmx.de>
-David Ostrovsky <david@ostrovsky.org> <david.ostrovsky@gmail.com>
-David Pursehouse <dpursehouse@collab.net> <david.pursehouse@sonymobile.com>
-Deniz Türkoglu <deniz@spotify.com> Deniz Türkoglu <deniz@spotify.com>
-Deniz Türkoglu <deniz@spotify.com> Deniz Turkoglu <deniz@spotify.com>
-Doug Kelly <dougk.ff7@gmail.com> <doug.kelly@garmin.com>
-Edwin Kempin <ekempin@google.com> Edwin Kempin <edwin.kempin@gmail.com>
-Edwin Kempin <ekempin@google.com> Edwin Kempin <edwin.kempin@sap.com>
-Edwin Kempin <ekempin@google.com> ekempin <ekempin@google.com>
-Eryk Szymanski <eryksz@gmail.com> <eryksz@google.com>
-Fredrik Luthander <fredrik.luthander@sonymobile.com> <fredrik@gandaraj.com>
-Fredrik Luthander <fredrik.luthander@sonymobile.com> <fredrik.luthander@sonyericsson.com>
-Gerrit Code Review <no-reply@gerritcodereview.com> <noreply-gerritcodereview@google.com>
-Gustaf Lundh <gustaflh@axis.com> <gustaf.lundh@axis.com>
-Gustaf Lundh <gustaflh@axis.com> <gustaf.lundh@sonyericsson.com>
-Gustaf Lundh <gustaflh@axis.com> <gustaf.lundh@sonymobile.com>
-Han-Wen Nienhuys <hanwen@google.com> <hanwen@google.com>
-Hector Oswaldo Caballero <hector.caballero@ericsson.com> <hector.caballero@ericsson.com>
-Hugo Arès <hugo.ares@ericsson.com> Hugo Ares <hugo.ares@ericsson.com>
-Hugo Arès <hugo.ares@ericsson.com> <hugares@gmail.com>
-Jacek Centkowski <jcentkowski@collab.net> <gemincia.programs@gmail.com>
-Jacek Centkowski <jcentkowski@collab.net> <geminica.programs@gmail.com>
-James E. Blair <jeblair@redhat.com> <jeblair@hp.com>
-Jason Huntley <jhuntley@houghtonassociates.com> jhuntley <jhuntley@houghtonassociates.com>
-Jiří Engelthaler <EngyCZ@gmail.com> <engycz@gmail.com>
-Joe Onorato <onoratoj@gmail.com> <joeo@android.com>
-Joel Dodge <dodgejoel@gmail.com> dodgejoel <dodgejoel@gmail.com>
-Johan Björk <jbjoerk@gmail.com> Johan Bjork <phb@spotify.com>
-JT Olds <hello@jtolds.com> <jtolds@gmail.com>
-Kasper Nilsson <kaspern@google.com> <kaspern@google.com>
-Lawrence Dubé <ldube@audiokinetic.com> <ldube@audiokinetic.com>
-Lei Sun <lei.sun01@sap.com> LeiSun <lei.sun01@sap.com>
-Lincoln Oliveira Campos Do Nascimento <lincoln.oliveiracamposdonascimento@sonyericsson.com> lincoln <lincoln.oliveiracamposdonascimento@sonyericsson.com>
-Luca Milanesio <luca.milanesio@gmail.com> <luca@gitent-scm.com>
-Magnus Bäck <magnus.back@axis.com> <baeck@google.com>
-Magnus Bäck <magnus.back@axis.com> <magnus.back@sonyericsson.com>
-Marco Miller <marco.miller@ericsson.com> <marco.mmiller@gmail.com>
-Mark Derricutt <mark.derricutt@smxemail.com> <mark@talios.com>
-Martin Fick <mfick@codeaurora.org> <mogulguy10@gmail.com>
-Martin Fick <mfick@codeaurora.org> <mogulguy@yahoo.com>
-Martin Wallgren <martinwa@axis.com> <martin.wallgren@axis.com>
-Matthias Sohn <matthias.sohn@sap.com> <matthias.sohn@gmail.com>
-Maxime Guerreiro <maximeg@google.com> <maximeg@google.com>
-Michael Zhou <moz@google.com> <zhoumotongxue008@gmail.com>
-Monty Taylor <mordred@inaugust.com> <monty.taylor@gmail.com>
-Mônica Dionísio <monica.dionisio@sonyericsson.com> monica.dionisio <monica.dionisio@sonyericsson.com>
-Nasser Grainawi <nasser@grainawi.org> <nasser@codeaurora.org>
-Nasser Grainawi <nasser@grainawi.org> <nasserg@quicinc.com>
-Orgad Shaneh <orgads@gmail.com> <orgad.shaneh@audiocodes.com>
-Paladox <thomasmulhall410@yahoo.com> <thomasmulhall410@yahoo.com>
-Patrick Hiesel <hiesel@google.com> <hiesel@hiesel-macbookpro2.roam.corp.google.com>
-Peter Jönsson <peter.joensson@gmail.com> Peter Jönsson <peter.joensson@gmail.com>
-Rafael Rabelo Silva <rafael.rabelosilva@sonyericsson.com> rafael.rabelosilva <rafael.rabelosilva@sonyericsson.com>
-Réda Housni Alaoui <reda.housnialaoui@gmail.com> <alaoui.rda@gmail.com>
-Richard Möhn <richard.moehn@posteo.de> <richard.moehn@fu-berlin.de>
-Sam Saccone <samccone@google.com> <samccone@gmail.com>
-Sam Saccone <samccone@google.com> <samccone@google.com>
-Saša Živkov <sasa.zivkov@sap.com> Sasa Zivkov <sasa.zivkov@sap.com>
-Saša Živkov <sasa.zivkov@sap.com> Saša Živkov <zivkov@gmail.com>
-Saša Živkov <sasa.zivkov@sap.com> Sasa Zivkov <zivkov@gmail.com>
-Scott Dial <scott@scottdial.com> <geekmug@gmail.com>
-Shawn Pearce <sop@google.com> Shawn O. Pearce <sop@google.com>
-Sixin Li <sixin210@gmail.com> sixin li <sixin210@gmail.com>
-Sven Selberg <svense@axis.com> <sven.selberg@axis.com>
-Sven Selberg <svense@axis.com> <sven.selberg@sonymobile.com>
-Thomas Dräbing <thomas.draebing@sap.com> <thomas.draebing@sap.com>
-Tom Wang <twang10@gmail.com> Tom <twang10@gmail.com>
-Tomas Westling <thomas.westling@sonyericsson.com> thomas.westling <thomas.westling@sonyericsson.com>
-Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> <ulrik.sjolin@gmail.com>
-Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@gmail.com>
-Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>
-Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@sonyericsson.com>
-Viktar Donich <viktard@google.com> viktard
-Yuxuan 'fishy' Wang <fishywang@google.com> Yuxuan Wang <fishywang@google.com>
-Zalán Blénessy <zalanb@axis.com> Zalan Blenessy <zalanb@axis.com>
-飞 李 <lifei@7v1.net> lifei <lifei@7v1.net>
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index 050118b..65f05b1 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -7,7 +7,6 @@
[verse]
--
_ssh_ -p <port> <host> _gerrit show-caches_
- [--gc]
[--show-jvm]
--
@@ -15,10 +14,6 @@
Display statistics about the size and hit ratio of in-memory caches.
== OPTIONS
---gc::
- Request Java garbage collection before displaying information
- about the Java memory heap.
-
--show-jvm::
List the name and version of the Java virtual machine, host
operating system, and other details about the environment
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 7864858..32c05b0 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1415,6 +1415,13 @@
+
The default limit is 16kiB.
+[[change.topicLimit]]change.topicLimit::
++
+Maximum allowed number of changes with the same topic. 0 or negative values
+mean "unlimited".
++
+By default 5,000.
+
[[change.cumulativeCommentSizeLimit]]change.cumulativeCommentSizeLimit::
+
Maximum allowed size in characters of all comments (including robot comments)
@@ -1443,6 +1450,16 @@
+
By default 100,000.
+[[change.maxFileSizeDownload]]change.maxFileSizeDownload::
++
+The link:rest-api-changes.html#get-content[GetContent] and
+link:rest-api-changes.html#get-safe-content[DownloadContent] REST APIs will
+refuse to load files larger than this limit (in bytes). 0 or negative values
+mean "unlimited".
+
++
+By default 0 (unlimited).
+
[[change.maxPatchSets]]change.maxPatchSets::
+
Maximum number of patch sets allowed per change. If this is insufficient,
@@ -1655,6 +1672,16 @@
+
Default is 5 minutes.
+[[change.diff3ConflictView]]change.diff3ConflictView::
++
+Use the diff3 formatter for merge commits with conflicts. With diff3 when
+the conflicts are shown in the "Auto Merge" view, the base section from the
+common parents will be shown as well.
+This setting takes effect when generating the automerge, which happens on upload.
+Changing the setting leaves existing changes unaffected.
++
+Default is false.
+
[[changeCleanup]]
=== Section changeCleanup
@@ -3373,6 +3400,8 @@
+
Index queries are repeated with a non-zero offset to obtain the
next set of results.
+_Note: Results may be inaccurate if the data-set is changing during the query
+execution._
+
* `SEARCH_AFTER`
+
@@ -3380,9 +3409,30 @@
backends can provide their custom implementations for search-after.
Note that, `SEARCH_AFTER` does not impact using offsets in Gerrit
query APIs.
+_Note: Depending on the index backend and its settings, results may be
+inaccurate if the data-set is changing during the query execution._
++
+* `NONE`
++
+Index queries are executed returning all results, without internal
+pagination.
+_Note: Since the entire set of indexing results is kept in memory
+during the API execution, this option may lead to higher memory utilisation
+and overall reduced performance.
+Bear in mind that some indexing backends may not support unbounded queries;
+therefore, the NONE option is unavailable._
+
Defaults to `OFFSET`.
+[[index.defaultLimit]]index.defaultLimit::
++
+Default limit, if the user does not provide a limit. If this is not set or set
+to 0, then index queries are executed with the maximum permitted limit for the
+user, which may be really high and cause too much load on the index. Thus
+setting this default limit to something smaller like 100 allows you to control
+the load, while not taking away any permission from the user. If the user
+provides a limit themselves, then `defaultLimit` is ignored.
+
[[index.maxLimit]]index.maxLimit::
+
Maximum limit to allow for search queries. Requesting results above this
@@ -3425,6 +3475,8 @@
For example, if the limit of the previous query was 500 and pageSizeMultiplier
is configured to 5, the next query will have a limit of 2500.
+
+_Note: ignored when paginationType is `NONE`_
++
Defaults to 1 which effectively turns this feature off.
[[index.maxPageSize]]index.maxPageSize::
@@ -3437,6 +3489,8 @@
configured to 5 and maxPageSize to 2000, the next query will have a limit of
2000 (instead of 2500).
+
+_Note: ignored when paginationType is `NONE`_
++
Defaults to no limit.
[[index.maxTerms]]index.maxTerms::
@@ -3880,11 +3934,12 @@
example to join given name and surname together, use the pattern
`${givenName} ${SN}`.
+
-If set, users will be unable to modify their full name field, as
-Gerrit will populate it only from the LDAP data.
-+
Default is `displayName` for FreeIPA and RFC 2307 servers,
and `${givenName} ${sn}` for Active Directory.
++
+A non-empty or default value prevents users from modifying their full
+name field. To allow edits to the full name field, set to the empty
+string.
[[ldap.accountEmailAddress]]ldap.accountEmailAddress::
+
@@ -4624,6 +4679,35 @@
If no keys are specified, web-of-trust checks are disabled. This is the
default behavior.
+[[receive.enableChangeIdLinkFooters]]receive.enableChangeIdLinkFooters::
++
+Enables a `Link` footer to be used as an alternative change ID footer.
++
+In some projects it may be desirable for the footer to contain a link to
+the Gerrit review page so that it is convenient to access the review
+page starting from the commit message. The `Link` footer is a standard
+footer used for inserting links in the commit message (e.g. used by the
+Linux kernel).
++
+This option makes Gerrit interoperate well with `Link` footers. If
+change ID `Link` footers are enabled Gerrit recognizes footers of the
+form:
++
+----
+ Link: https://<host>/id/<change-ID>
+----
++
+Example:
+----
+ Link: https://gerrit-review.googlesource.com/id/I78e884a944cedb5144f661a057e4829c8f84e933
+----
++
+For Gerrit to recognize the change ID, the part of the URL before the
+'/id/' part must match with the link:#gerrit.canonicalWebUrl[canonical
+web URL].
++
+Default is `true`.
+
[[repository]]
=== Section repository
diff --git a/Documentation/config-groups.txt b/Documentation/config-groups.txt
index 4abb223..40f64da 100644
--- a/Documentation/config-groups.txt
+++ b/Documentation/config-groups.txt
@@ -93,10 +93,12 @@
== Pushing to group refs
-Validation on push for changes to the group ref is not implemented, so
-pushes are rejected. Pushes that bypass Gerrit should be avoided since
-the names, IDs and UUIDs must be internally consistent between all the
-branches involved. In addition, group references should not be created
+Users can push changes to `refs/for/refs/groups/*`, but submit is rejected
+for changes which update group files (i.e. group.config, members, subgroups).
+It is possible for users to upload and submit changes on the named destination
+or named query files in a group ref. Pushes that bypass Gerrit should be
+avoided since the names, IDs and UUIDs must be internally consistent between
+all the branches involved. In addition, group references should not be created
or deleted manually either. If you attempt any of these actions
anyway, don't forget to link:rest-api-groups.html#index-group[Index
Group] reindex the affected groups manually.
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 7b1baba..218affd 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -69,8 +69,8 @@
The link:#project-section[+project+ section] appears once per project.
-The link:#access-section[+access+ section] appears once per reference pattern,
-such as `+refs/*+` or `+refs/heads/*+`. Only one access section per pattern is
+The link:#access-subsection[+access+ section] appears once per reference pattern,
+such as `+refs/*+` or `+refs/heads/*+`. Only one access section per pattern is
allowed.
The link:#receive-section[+receive+ section] appears once per project.
@@ -317,7 +317,9 @@
The submit section includes configuration of project-specific
submit settings:
-[[content_merge]]submit.mergeContent::
+[[content_merge]]
+
+[[submit.mergeContent]]submit.mergeContent::
+
Defines whether Gerrit will try to do a content merge when a path conflict
occurs while submitting a change.
@@ -482,7 +484,7 @@
to the change in the web UI (see link:#submit-footers[below]).
+
The footers that are added are exactly the same footers that are also added by
-the link:cherry_pick[cherry pick] action. Thus, the `rebase always` action can
+the link:#cherry_pick[cherry pick] action. Thus, the `rebase always` action can
be considered similar to the `cherry pick` action, but with the important
distinction that `rebase always` does not ignore dependencies, which is why
using the `rebase always` action should be preferred over the `cherry pick`
@@ -635,8 +637,17 @@
[[access-section]]
=== Access section
-Each +access+ section includes a reference and access rights connected
-to groups. Each group listed must exist in the link:#file-groups[+groups+ file].
+[[access.inheritFrom]]access.inheritFrom::
++
+Name of the parent project from which access rights are inherited.
++
+If not set, access rights are inherited from the `All-Projects` root project.
+
+[[access-subsection]]
+==== Access subsection
+
++access+ subsections for references connect access rights to groups. Each group
+listed must exist in the link:#file-groups[+groups+ file].
Please refer to the
link:access-control.html#access_categories[Access Categories]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 015faab..cc7cbd0 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -457,6 +457,10 @@
+
Update of the change secondary index
+* `com.google.gerrit.server.extensions.events.CustomKeyedValueValidationListener`:
++
+Updates to custom keyed values
+
* `com.google.gerrit.server.extensions.events.AccountIndexedListener`:
+
Update of the account secondary index
@@ -3036,6 +3040,35 @@
`com.google.gerrit.server.RequestListener` is an extension point that is
invoked each time the server executes a request from a user.
+[[custom-keyed-values]]
+== Custom Keyed values
+
+It is possible to associate custom keyed values with a change. This is a map
+from string to string, allowing for the storage of small keys and values. An
+example would be for storing an associated workspace with the given change.
+
+As an example:
+```
+ private void setWorkspace(ChangeResource rsrc, String workspaceId)
+ throws RestApiException, UpdateException {
+ try (RefUpdateContext pluginCtx = RefUpdateContext.open(PLUGIN);
+ RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION);
+ BatchUpdate bu =
+ updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
+ SetCustomKeyedValuesOp op =
+ customKeyedValuesFactory.create(
+ new CustomKeyedValuesInput(ImmutableMap.of("workspace", workspaceId)));
+ bu.addOp(rsrc.getId(), op);
+ bu.execute();
+ }
+ }
+```
+
+These custom-keyed-values can be fetched by passing the option `o=CUSTOM_KEYED_VALUES`
+to a change details fetch.
+
+
+
== SEE ALSO
* link:pg-plugin-dev.html[JavaScript Plugin API]
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index f13bc22..97b58af 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -125,7 +125,7 @@
and link:access-control.html#references_magic[magic refs].
Gerrit only supports tags that are reachable by any ref not owned by
-Gerrit. This includes branches (refs/heads/*) or custom ref namespaces
+Gerrit. This includes branches (refs/heads/\*) or custom ref namespaces
(refs/my-company/*). Tagging a change ref is not supported.
When filtering tags by visibility, Gerrit performs a reachability check
and will present the user ony with tags that are reachable by any ref
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 2499a15..5e6a540 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -99,6 +99,7 @@
* javaewah
* jsr305
* mime-util
+* roaringbitmap
* servlet-api
* servlet-api-without-neverlink
* soy
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 2f43538..0d72447 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -351,7 +351,14 @@
* `cache_used_per_repository` : Bytes of memory retained per repository for the
top N repositories having most data in the cache. The number N of reported
repositories is limited to 1000.
-** `repository_name`: The name of the repository.
+** `repository_name`: The name of the repository. Note that it is a subject of
+ sanitization in order to avoid collision between repository names. Rules
+ are:
+*** any character outside `[a-zA-Z0-9_-]+([a-zA-Z0-9_-]+)*` pattern is replaced
+ with `\_0x[HEX CODE]_` (code is capitalized) string
+*** for instance `repo/name` is sanitized to `repo_0x2F_name`
+*** if repository name contains the replacement prefix (`_0x`) it is prefixed
+ with another `_0x` e.g. `repo_0x2F_name` becomes `repo_0x_0x2F_name`
=== Git
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index af922f1..ce736c0 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2855,14 +2855,14 @@
[[get-custom-keyed-values]]
=== Get Custom Keyed Values
--
-'GET /changes/link:#change-id[\{change-id\}]/custom-keyed-values'
+'GET /changes/link:#change-id[\{change-id\}]/custom_keyed_values'
--
Gets the custom keyed values associated with a change.
.Request
----
- GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/custom-keyed-values HTTP/1.0
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/custom_keyed_values HTTP/1.0
----
As response the change's custom keyed values are returned as a map of strings.
@@ -2883,7 +2883,7 @@
[[set-custom-keyed-values]]
=== Set Custom Keyed Values
--
-'POST /changes/link:#change-id[\{change-id\}]/custom-keyed-values'
+'POST /changes/link:#change-id[\{change-id\}]/custom_keyed_values'
--
Adds and/or removes custom keyed values from a change.
@@ -2893,7 +2893,7 @@
.Request
----
- POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/custom-keyed-values HTTP/1.0
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/custom_keyed_values HTTP/1.0
Content-Type: application/json; charset=UTF-8
{
@@ -6676,22 +6676,22 @@
[[change-id]]
=== \{change-id\}
Identifier that uniquely identifies one change. It contains the URL-encoded
-project name as well as the change number: "'$$<project>~<changeNumber>$$'"
+project name as well as the change number: "<project>~<changeNumber>"
==== Alternative identifiers
Gerrit also supports an array of other change identifiers.
[NOTE]
Even though these identifiers will work in the majority of cases it is highly
-recommended to use "'$$<project>~<changeNumber>$$'" whenever possible.
+recommended to use "<project>~<changeNumber>" whenever possible.
Since these identifiers require additional lookups from index and caches, to
-be translated to the "'$$<project>~<changeNumber>$$'" identifier, they
+be translated to the "<project>~<changeNumber>" identifier, they
may result in both false-positives and false-negatives.
Furthermore the additional lookup mean that they come with a performance penalty.
-* an ID of the change in the format "'$$<project>~<branch>~<Change-Id>$$'",
+* an ID of the change in the format "<project>~<branch>~<Change-Id>",
where for the branch the `refs/heads/` prefix can be omitted
- ("$$myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940$$")
+ ("myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940")
* a Change-Id if it uniquely identifies one change
("I8473b95934b5732ac55d26311a706c9c2bde9940")
* a change number if it uniquely identifies one change ("4247")
@@ -7060,11 +7060,8 @@
The user who submitted the change, as an
link:rest-api-accounts.html#account-info[ AccountInfo] entity.
|`starred` |not set if `false`|
-Whether the calling user has starred this change with the default label.
+Whether the calling user has starred this change.
Only set if link:#star[requested].
-|`stars` |optional|
-A list of star labels that are applied by the calling user to this
-change. The labels are lexicographically sorted.
|`reviewed` |not set if `false`|
Whether the change was reviewed by the calling user.
Only set if link:#reviewed[reviewed] is requested.
@@ -8136,6 +8133,14 @@
The caller needs "Forge Author" permission when using this field.
This field does not affect the owner or the committer of the change, which will
continue to use the identity of the caller.
+|`validation_options` |optional|
+Map with key-value pairs that are forwarded as options to the commit validation
+listeners (e.g. can be used to skip certain validations). Which validation
+options are supported depends on the installed commit validation listeners.
+Gerrit core doesn't support any validation options, but commit validation
+listeners that are implemented in plugins may. Please refer to the
+documentation of the installed plugins to learn whether they support validation
+options. Unknown validation options are silently ignored.
|==================================
[[move-input]]
@@ -8575,6 +8580,8 @@
If set to true, ignore all automatic attention set rules described in the
link:#attention-set[attention set]. Updates in add_to_attention_set
and remove_from_attention_set are not ignored.
+|`response_format_options` |optional|
+List of link:#query-options[query options] to format the response.
|============================
[[review-result]]
@@ -8598,6 +8605,9 @@
action. Not set if false.
|`error` |optional|
Error message for non-200 responses.
+|`change_info` |optional|
+Post-update change information. Only set if `response_format_options` were
+set.
|============================
[[reviewer-info]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index a347d6c..1f8e696 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -721,11 +721,6 @@
+
Includes a JVM summary.
-* `gc`:
-+
-Requests a Java garbage collection before computing the information
-about the Java memory heap.
-
.Request
----
GET /config/server/summary?jvm HTTP/1.0
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 9e71df7..528be41 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -139,11 +139,16 @@
)]}'
{
"some-project": {
- "id": "some-project"
+ "id": "some-project",
+ _more_projects: true
}
}
----
+If the number of projects matching the query exceeds either the
+internal limit or a supplied `limit` query parameter, the last project
+object has a `_more_projects: true` JSON field set.
+
[[suggest-projects]]
Prefix(p)::
@@ -420,6 +425,10 @@
GET /projects/?query=<query>&limit=25 HTTP/1.0
----
+If the number of projects matching the query exceeds either the
+internal limit or a supplied `limit` query parameter, the last project
+object has a `_more_projects: true` JSON field set.
+
The `/projects/` URL also accepts a start integer in the `start`
parameter. The results will skip `start` projects from project list.
@@ -4370,28 +4379,31 @@
The `ProjectInfo` entity contains information about a project.
[options="header",cols="1,^2,4"]
-|===========================
-|Field Name ||Description
-|`id` ||The URL encoded project name.
-|`name` |
+|=============================
+|Field Name ||Description
+|`id` ||The URL encoded project name.
+|`name` |
not set if returned in a map where the project name is used as map key|
The name of the project.
-|`parent` |optional|
+|`parent` |optional|
The name of the parent project. +
`?-<n>` if the parent project is not visible (`<n>` is a number which
is increased for each non-visible project).
-|`description` |optional|The description of the project.
-|`state` |optional|`ACTIVE`, `READ_ONLY` or `HIDDEN`.
-|`branches` |optional|Map of branch names to HEAD revisions.
-|`labels` |optional|
+|`description` |optional|The description of the project.
+|`state` |optional|`ACTIVE`, `READ_ONLY` or `HIDDEN`.
+|`branches` |optional|Map of branch names to HEAD revisions.
+|`labels` |optional|
Map of label names to
link:#label-type-info[LabelTypeInfo] entries.
This field is filled for link:#create-project[Create Project] and
link:#get-project[Get Project] calls.
-|`web_links` |optional|
+|`web_links` |optional|
Links to the project in external sites as a list of
link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
-|===========================
+|`_more_projects`|optional, not set if `false`|
+Whether the query would deliver more results if not limited. +
+Only set on the last project that is returned.
+|=============================
[[project-input]]
=== ProjectInput
diff --git a/Documentation/user-named-destinations.txt b/Documentation/user-named-destinations.txt
index a1ab258..cee562b 100644
--- a/Documentation/user-named-destinations.txt
+++ b/Documentation/user-named-destinations.txt
@@ -1,15 +1,19 @@
= Gerrit Code Review - Named Destinations
-[[user-named-destinations]]
-== User Named Destinations
-It is possible to define named destination sets on a user level.
+[[user-or-group-named-destinations]]
+== User Or Group Named Destinations
+It is possible to define named destination sets on a user or group level.
To do this, define the named destination sets in files named after
each destination set in the `destinations` directory of the user's
-account ref in the `All-Users` project. The user's account ref is
-based on the user's account id which is an integer. The account
-refs are sharded by the last two digits (`+nn+`) in the refname,
-leading to refs of the format `+refs/users/nn/accountid+`. The
-user's destination files are a 2 column tab delimited file. Each
+or group's account ref in the `All-Users` project. The user's account ref is
+based on the user's account id which is an integer. The user account refs
+are sharded by the last two digits (`+nn+`) in the refname, leading to refs
+of the format `+refs/users/nn/accountid+`. Similarly, the group's ref is
+based on the group id which is a UUID. The group refs are sharded
+by the first 2 characters of the group UUID, leading to a refs of the
+format `+refs/groups/cc/groupid+`.
+
+The destination files are a 2 column tab delimited file. Each
row in a destination file represents a single destination in the
named set. The left column represents the ref of the destination,
and the right column represents the project of the destination.
diff --git a/Documentation/user-named-queries.txt b/Documentation/user-named-queries.txt
index c01f790..938cd53 100644
--- a/Documentation/user-named-queries.txt
+++ b/Documentation/user-named-queries.txt
@@ -1,11 +1,12 @@
= Gerrit Code Review - Named Queries
-[[user-named-queries]]
-== User Named Queries
-It is possible to define named queries on a user level. To do
+[[user-or-group-named-queries]]
+== User Or Group Named Queries
+It is possible to define named queries on a user or group level. To do
this, define the named queries in the `queries` file under the
-link:intro-user.html#user-refs[user's ref] in the `All-Users` project. The
-user's queries file is a 2 column tab delimited file. The left
+link:intro-user.html#user-refs[user's ref] or
+link:config-groups.html#_storage_format[group's ref] in the `All-Users`
+project. The named queries file is a 2 column tab delimited file. The left
column represents the name of the query, and the right column
represents the query expression represented by the name. The named queries
can be publicly accessible by other users.
diff --git a/Documentation/user-search-projects.txt b/Documentation/user-search-projects.txt
index 8ebbf3e..aabbd55 100644
--- a/Documentation/user-search-projects.txt
+++ b/Documentation/user-search-projects.txt
@@ -12,6 +12,17 @@
+
Matches projects that have exactly the name 'NAME'.
+[[prefix]]
+prefix:'PREFIX'::
++
+Matches projects that have a name that starts with 'PREFIX' (may be
+case-sensitive, depending on which index backend is used).
+
+[[substring]]
+substring:'SUBSTRING'::
++
+Matches projects that have a name that contains 'SUBSTRING' (case-insensitive).
+
[[parent]]
parent:'PARENT'::
+
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 67b8d75..9ed4792 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -128,11 +128,13 @@
as a change number such as 15183, or a Change-Id from the Change-Id footer.
[[destination]]
-destination:'[name=]NAME[,user=USER]'::
+destination:'[name=]NAME[,user=USER|,group=GROUP]'::
+
-Changes which match the specified USER's destination named 'NAME'. If 'USER'
-is unspecified, the current user is used. The named destinations can be
-publicly accessible by other users.
+Changes which match the specified USER's or GROUP's destination named 'NAME'.
+If 'USER' is unspecified, the current user is used. The named destinations can
+be publicly accessible by other users.
+The value may be wrapped in double quotes to include spaces. For example,
+`destination:"myreviews,group=My Group"`
(see link:user-named-destinations.html[Named Destinations]).
[[owner]]
@@ -160,11 +162,13 @@
'GROUP'.
[[query]]
-query:'[name=]NAME[,user=USER]'::
+query:'[name=]NAME[,user=USER|,group=GROUP]'::
+
-Changes which match the specified USER's query named 'NAME'. If 'USER'
-is unspecified, the current user is used. The named queries can be
-publicly accessible by other users.
+Changes which match the specified USER's or GROUP's query named 'NAME'.
+If neither 'USER' nor 'GROUP' is specified, the current user is used.
+The named queries can be publicly accessible by other users.
+The value may be wrapped in double quotes to include spaces. For example,
+`query:"myquery,group=My Group"`
(see link:user-named-queries.html[Named Queries]).
[[reviewer]]
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 36bc3c4..a0cda1a 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -452,6 +452,13 @@
bind(TestTicker.class).toInstance(testTicker);
}
});
+ // Assure that HTTPD is enabled if SSHD is not required. If both are disabled the Gerrit server
+ // does not start. Alternatively we could assure that SSHD is enabled if HTTPD is not required,
+ // but this would break the tests at Google, because they don't have support for SSHD.
+ daemon.setEnableHttpd(desc.httpd() || !desc.useSsh());
+ daemon.setEnableSshd(desc.useSsh());
+ daemon.setReplica(
+ ReplicaUtil.isReplica(baseConfig) || ReplicaUtil.isReplica(desc.buildConfig(baseConfig)));
if (desc.memory()) {
checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
@@ -468,7 +475,6 @@
@Nullable InMemoryRepositoryManager inMemoryRepoManager)
throws Exception {
Config cfg = desc.buildConfig(baseConfig);
- daemon.setReplica(ReplicaUtil.isReplica(baseConfig) || ReplicaUtil.isReplica(cfg));
mergeTestConfig(cfg);
// Set the log4j configuration to an invalid one to prevent system logs
// from getting configured and creating log files.
@@ -576,6 +582,7 @@
cfg.setString("gerrit", null, "basePath", "git");
cfg.setBoolean("sendemail", null, "enable", true);
cfg.setInt("sendemail", null, "threadPoolSize", 0);
+ cfg.setInt("execution", null, "fanOutThreadPoolSize", 0);
cfg.setInt("plugins", null, "checkFrequency", 0);
cfg.setInt("sshd", null, "threads", 1);
@@ -745,4 +752,8 @@
public String toString() {
return MoreObjects.toStringHelper(this).addValue(desc).toString();
}
+
+ public boolean isReplica() {
+ return daemon.isReplica();
+ }
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperationsImpl.java
index db264c5..3d53816 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerDraftCommentOperationsImpl.java
@@ -18,7 +18,7 @@
import static com.google.gerrit.acceptance.testsuite.change.PerCommentOperationsImpl.toTestComment;
import com.google.gerrit.entities.HumanComment;
-import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -30,10 +30,9 @@
* the separation between interface and implementation to enhance clarity.
*/
public class PerDraftCommentOperationsImpl implements PerDraftCommentOperations {
- private final CommentsUtil commentsUtil;
-
private final ChangeNotes changeNotes;
private final String commentUuid;
+ private final DraftCommentsReader draftCommentsReader;
public interface Factory {
PerDraftCommentOperationsImpl create(ChangeNotes changeNotes, String commentUuid);
@@ -41,16 +40,18 @@
@Inject
public PerDraftCommentOperationsImpl(
- CommentsUtil commentsUtil, @Assisted ChangeNotes changeNotes, @Assisted String commentUuid) {
- this.commentsUtil = commentsUtil;
+ DraftCommentsReader draftCommentsReader,
+ @Assisted ChangeNotes changeNotes,
+ @Assisted String commentUuid) {
this.changeNotes = changeNotes;
this.commentUuid = commentUuid;
+ this.draftCommentsReader = draftCommentsReader;
}
@Override
public TestHumanComment get() {
HumanComment comment =
- commentsUtil.draftByChange(changeNotes).stream()
+ draftCommentsReader.getDraftsByChangeForAllAuthors(changeNotes).stream()
.filter(foundComment -> foundComment.key.uuid.equals(commentUuid))
.collect(onlyElement());
return toTestComment(comment);
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index 1099919..8f930bb 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -23,6 +23,7 @@
":annotations",
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/launcher",
"//java/com/google/gerrit/prettify:server",
"//lib:guava",
"//lib:jgit",
diff --git a/java/com/google/gerrit/common/IoUtil.java b/java/com/google/gerrit/common/IoUtil.java
index 09a8993..e51a6e5 100644
--- a/java/com/google/gerrit/common/IoUtil.java
+++ b/java/com/google/gerrit/common/IoUtil.java
@@ -14,20 +14,9 @@
package com.google.gerrit.common;
-import com.google.common.collect.Sets;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
public final class IoUtil {
public static void copyWithThread(InputStream src, OutputStream dst) {
@@ -64,50 +53,5 @@
}.start();
}
- public static void loadJARs(Collection<Path> jars) {
- if (jars.isEmpty()) {
- return;
- }
-
- ClassLoader cl = IoUtil.class.getClassLoader();
- if (!(cl instanceof URLClassLoader)) {
- throw noAddURL("Not loaded by URLClassLoader", null);
- }
-
- @SuppressWarnings("resource") // Leave open so classes can be loaded.
- URLClassLoader urlClassLoader = (URLClassLoader) cl;
-
- Method addURL;
- try {
- addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
- addURL.setAccessible(true);
- } catch (SecurityException | NoSuchMethodException e) {
- throw noAddURL("Method addURL not available", e);
- }
-
- Set<URL> have = Sets.newHashSet(Arrays.asList(urlClassLoader.getURLs()));
- for (Path path : jars) {
- try {
- URL url = path.toUri().toURL();
- if (have.add(url)) {
- addURL.invoke(cl, url);
- }
- } catch (MalformedURLException | IllegalArgumentException | IllegalAccessException e) {
- throw noAddURL("addURL " + path + " failed", e);
- } catch (InvocationTargetException e) {
- throw noAddURL("addURL " + path + " failed", e.getCause());
- }
- }
- }
-
- public static void loadJARs(Path jar) {
- loadJARs(Collections.singleton(jar));
- }
-
- private static UnsupportedOperationException noAddURL(String m, Throwable why) {
- String prefix = "Cannot extend classpath: ";
- return new UnsupportedOperationException(prefix + m, why);
- }
-
private IoUtil() {}
}
diff --git a/java/com/google/gerrit/common/JarUtil.java b/java/com/google/gerrit/common/JarUtil.java
new file mode 100644
index 0000000..b88a7ab
--- /dev/null
+++ b/java/com/google/gerrit/common/JarUtil.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2023 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.common;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.launcher.GerritLauncher.GerritClassLoader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/** Provides util methods for dynamic loading jars */
+public final class JarUtil {
+ public static void loadJars(Collection<Path> jars) {
+ if (jars.isEmpty()) {
+ return;
+ }
+
+ ClassLoader cl = JarUtil.class.getClassLoader();
+ if (!(cl instanceof GerritClassLoader)) {
+ throw noAddURL("Not loaded by GerritClassLoader", null);
+ }
+
+ @SuppressWarnings("resource") // Leave open so classes can be loaded.
+ GerritClassLoader gerritClassLoader = (GerritClassLoader) cl;
+
+ Set<URL> have = Sets.newHashSet(Arrays.asList(gerritClassLoader.getURLs()));
+ for (Path path : jars) {
+ try {
+ URL url = path.toUri().toURL();
+ if (have.add(url)) {
+ gerritClassLoader.addURL(url);
+ }
+ } catch (MalformedURLException | IllegalArgumentException e) {
+ throw noAddURL("addURL " + path + " failed", e);
+ }
+ }
+ }
+
+ public static void loadJars(Path jar) {
+ loadJars(Collections.singleton(jar));
+ }
+
+ private static UnsupportedOperationException noAddURL(String m, Throwable why) {
+ String prefix = "Cannot extend classpath: ";
+ return new UnsupportedOperationException(prefix + m, why);
+ }
+
+ private JarUtil() {}
+}
diff --git a/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java b/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
index fa9b139..95df5be 100644
--- a/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
+++ b/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
@@ -35,7 +35,7 @@
public static void loadSiteLib(Path libdir) {
try {
List<Path> jars = listJars(libdir);
- IoUtil.loadJARs(jars);
+ JarUtil.loadJars(jars);
logger.atFine().log("Loaded site libraries: %s", lazy(() -> jarList(jars)));
} catch (IOException e) {
logger.atSevere().withCause(e).log("Error scanning lib directory %s", libdir);
diff --git a/java/com/google/gerrit/common/data/FilenameComparator.java b/java/com/google/gerrit/common/data/FilenameComparator.java
index 0b188df..e1c763c0 100644
--- a/java/com/google/gerrit/common/data/FilenameComparator.java
+++ b/java/com/google/gerrit/common/data/FilenameComparator.java
@@ -30,6 +30,13 @@
@Override
public int compare(String path1, String path2) {
+ if (Patch.PATCHSET_LEVEL.equals(path1) && Patch.PATCHSET_LEVEL.equals(path2)) {
+ return 0;
+ } else if (Patch.PATCHSET_LEVEL.equals(path1)) {
+ return -1;
+ } else if (Patch.PATCHSET_LEVEL.equals(path2)) {
+ return 1;
+ }
if (Patch.COMMIT_MSG.equals(path1) && Patch.COMMIT_MSG.equals(path2)) {
return 0;
} else if (Patch.COMMIT_MSG.equals(path1)) {
diff --git a/java/com/google/gerrit/extensions/api/changes/CustomKeyedValuesInput.java b/java/com/google/gerrit/extensions/api/changes/CustomKeyedValuesInput.java
index a603328..740df21 100644
--- a/java/com/google/gerrit/extensions/api/changes/CustomKeyedValuesInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/CustomKeyedValuesInput.java
@@ -14,21 +14,21 @@
package com.google.gerrit.extensions.api.changes;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.restapi.DefaultInput;
+import java.util.Map;
+import java.util.Set;
public class CustomKeyedValuesInput {
- @DefaultInput public ImmutableMap<String, String> add;
- public ImmutableSet<String> remove;
+ @DefaultInput public Map<String, String> add;
+ public Set<String> remove;
public CustomKeyedValuesInput() {}
- public CustomKeyedValuesInput(ImmutableMap<String, String> add) {
+ public CustomKeyedValuesInput(Map<String, String> add) {
this.add = add;
}
- public CustomKeyedValuesInput(ImmutableMap<String, String> add, ImmutableSet<String> remove) {
+ public CustomKeyedValuesInput(Map<String, String> add, Set<String> remove) {
this(add);
this.remove = remove;
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 8bfe468..98807cb 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -18,7 +18,9 @@
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.FixSuggestionInfo;
import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -96,6 +98,8 @@
*/
public boolean ignoreAutomaticAttentionSetRules;
+ @Nullable public List<ListChangesOption> responseFormatOptions;
+
public enum DraftHandling {
/** Leave pending drafts alone. */
KEEP,
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewResult.java b/java/com/google/gerrit/extensions/api/changes/ReviewResult.java
index 95bea5b..bd22ca8 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewResult.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewResult.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.api.changes;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.ChangeInfo;
import java.util.Map;
/** Result object representing the outcome of a review request. */
@@ -38,4 +39,7 @@
/** Error message for non-200 responses. */
@Nullable public String error;
+
+ /** Change after applying the update. */
+ @Nullable public ChangeInfo changeInfo;
}
diff --git a/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java b/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
index 6d52a93..109afd6 100644
--- a/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -14,6 +14,9 @@
package com.google.gerrit.extensions.client;
+import com.google.common.base.MoreObjects;
+import java.util.Objects;
+
public class DiffPreferencesInfo {
/** Default number of lines of context. */
@@ -60,6 +63,97 @@
public Boolean skipUnchanged;
public Boolean skipUncommented;
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof DiffPreferencesInfo)) {
+ return false;
+ }
+ DiffPreferencesInfo other = (DiffPreferencesInfo) obj;
+ return Objects.equals(this.context, other.context)
+ && Objects.equals(this.tabSize, other.tabSize)
+ && Objects.equals(this.fontSize, other.fontSize)
+ && Objects.equals(this.lineLength, other.lineLength)
+ && Objects.equals(this.cursorBlinkRate, other.cursorBlinkRate)
+ && Objects.equals(this.expandAllComments, other.expandAllComments)
+ && Objects.equals(this.intralineDifference, other.intralineDifference)
+ && Objects.equals(this.manualReview, other.manualReview)
+ && Objects.equals(this.showLineEndings, other.showLineEndings)
+ && Objects.equals(this.showTabs, other.showTabs)
+ && Objects.equals(this.showWhitespaceErrors, other.showWhitespaceErrors)
+ && Objects.equals(this.syntaxHighlighting, other.syntaxHighlighting)
+ && Objects.equals(this.hideTopMenu, other.hideTopMenu)
+ && Objects.equals(this.autoHideDiffTableHeader, other.autoHideDiffTableHeader)
+ && Objects.equals(this.hideLineNumbers, other.hideLineNumbers)
+ && Objects.equals(this.renderEntireFile, other.renderEntireFile)
+ && Objects.equals(this.hideEmptyPane, other.hideEmptyPane)
+ && Objects.equals(this.matchBrackets, other.matchBrackets)
+ && Objects.equals(this.lineWrapping, other.lineWrapping)
+ && Objects.equals(this.ignoreWhitespace, other.ignoreWhitespace)
+ && Objects.equals(this.retainHeader, other.retainHeader)
+ && Objects.equals(this.skipDeleted, other.skipDeleted)
+ && Objects.equals(this.skipUnchanged, other.skipUnchanged)
+ && Objects.equals(this.skipUncommented, other.skipUncommented);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ context,
+ tabSize,
+ fontSize,
+ lineLength,
+ cursorBlinkRate,
+ expandAllComments,
+ intralineDifference,
+ manualReview,
+ showLineEndings,
+ showTabs,
+ showWhitespaceErrors,
+ syntaxHighlighting,
+ hideTopMenu,
+ autoHideDiffTableHeader,
+ hideLineNumbers,
+ renderEntireFile,
+ hideEmptyPane,
+ matchBrackets,
+ lineWrapping,
+ ignoreWhitespace,
+ retainHeader,
+ skipDeleted,
+ skipUnchanged,
+ skipUncommented);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper("DiffPreferencesInfo")
+ .add("context", context)
+ .add("tabSize", tabSize)
+ .add("fontSize", fontSize)
+ .add("lineLength", lineLength)
+ .add("cursorBlinkRate", cursorBlinkRate)
+ .add("expandAllComments", expandAllComments)
+ .add("intralineDifference", intralineDifference)
+ .add("manualReview", manualReview)
+ .add("showLineEndings", showLineEndings)
+ .add("showTabs", showTabs)
+ .add("showWhitespaceErrors", showWhitespaceErrors)
+ .add("syntaxHighlighting", syntaxHighlighting)
+ .add("hideTopMenu", hideTopMenu)
+ .add("autoHideDiffTableHeader", autoHideDiffTableHeader)
+ .add("hideLineNumbers", hideLineNumbers)
+ .add("renderEntireFile", renderEntireFile)
+ .add("hideEmptyPane", hideEmptyPane)
+ .add("matchBrackets", matchBrackets)
+ .add("lineWrapping", lineWrapping)
+ .add("ignoreWhitespace", ignoreWhitespace)
+ .add("retainHeader", retainHeader)
+ .add("skipDeleted", skipDeleted)
+ .add("skipUnchanged", skipUnchanged)
+ .add("skipUncommented", skipUncommented)
+ .toString();
+ }
+
public static DiffPreferencesInfo defaults() {
DiffPreferencesInfo i = new DiffPreferencesInfo();
i.context = DEFAULT_CONTEXT;
diff --git a/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java b/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
index 6672cb1..0a3ec0a 100644
--- a/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
@@ -14,6 +14,9 @@
package com.google.gerrit.extensions.client;
+import com.google.common.base.MoreObjects;
+import java.util.Objects;
+
/* This class is stored in Git config file. */
public class EditPreferencesInfo {
public Integer tabSize;
@@ -31,6 +34,67 @@
public Boolean autoCloseBrackets;
public Boolean showBase;
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof EditPreferencesInfo)) {
+ return false;
+ }
+ EditPreferencesInfo other = (EditPreferencesInfo) obj;
+ return Objects.equals(this.tabSize, other.tabSize)
+ && Objects.equals(this.lineLength, other.lineLength)
+ && Objects.equals(this.indentUnit, other.indentUnit)
+ && Objects.equals(this.cursorBlinkRate, other.cursorBlinkRate)
+ && Objects.equals(this.hideTopMenu, other.hideTopMenu)
+ && Objects.equals(this.showTabs, other.showTabs)
+ && Objects.equals(this.showWhitespaceErrors, other.showWhitespaceErrors)
+ && Objects.equals(this.syntaxHighlighting, other.syntaxHighlighting)
+ && Objects.equals(this.hideLineNumbers, other.hideLineNumbers)
+ && Objects.equals(this.matchBrackets, other.matchBrackets)
+ && Objects.equals(this.lineWrapping, other.lineWrapping)
+ && Objects.equals(this.indentWithTabs, other.indentWithTabs)
+ && Objects.equals(this.autoCloseBrackets, other.autoCloseBrackets)
+ && Objects.equals(this.showBase, other.showBase);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ tabSize,
+ lineLength,
+ indentUnit,
+ cursorBlinkRate,
+ hideTopMenu,
+ showTabs,
+ showWhitespaceErrors,
+ syntaxHighlighting,
+ hideLineNumbers,
+ matchBrackets,
+ lineWrapping,
+ indentWithTabs,
+ autoCloseBrackets,
+ showBase);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper("EditPreferencesInfo")
+ .add("tabSize", tabSize)
+ .add("lineLength", lineLength)
+ .add("indentUnit", indentUnit)
+ .add("cursorBlinkRate", cursorBlinkRate)
+ .add("hideTopMenu", hideTopMenu)
+ .add("showTabs", showTabs)
+ .add("showWhitespaceErrors", showWhitespaceErrors)
+ .add("syntaxHighlighting", syntaxHighlighting)
+ .add("hideLineNumbers", hideLineNumbers)
+ .add("matchBrackets", matchBrackets)
+ .add("lineWrapping", lineWrapping)
+ .add("indentWithTabs", indentWithTabs)
+ .add("autoCloseBrackets", autoCloseBrackets)
+ .add("showBase", showBase)
+ .toString();
+ }
+
public static EditPreferencesInfo defaults() {
EditPreferencesInfo i = new EditPreferencesInfo();
i.tabSize = 8;
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 020351b..5b33aca 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -14,7 +14,9 @@
package com.google.gerrit.extensions.client;
+import com.google.common.base.MoreObjects;
import java.util.List;
+import java.util.Objects;
/** Preferences about a single user. */
public class GeneralPreferencesInfo {
@@ -22,16 +24,6 @@
/** Default number of items to display per page. */
public static final int DEFAULT_PAGESIZE = 25;
- /** Preferred method to download a change. */
- public enum DownloadCommand {
- PULL,
- CHECKOUT,
- CHERRY_PICK,
- FORMAT_PATCH,
- BRANCH,
- RESET,
- }
-
public enum DateFormat {
/** US style dates: Apr 27, Feb 14, 2010 */
STD("MMM d", "MMM d, yyyy"),
@@ -186,6 +178,91 @@
return emailFormat;
}
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof GeneralPreferencesInfo)) {
+ return false;
+ }
+ GeneralPreferencesInfo other = (GeneralPreferencesInfo) obj;
+ return Objects.equals(this.changesPerPage, other.changesPerPage)
+ && Objects.equals(this.downloadScheme, other.downloadScheme)
+ && Objects.equals(this.theme, other.theme)
+ && Objects.equals(this.dateFormat, other.dateFormat)
+ && Objects.equals(this.timeFormat, other.timeFormat)
+ && Objects.equals(this.expandInlineDiffs, other.expandInlineDiffs)
+ && Objects.equals(this.relativeDateInChangeTable, other.relativeDateInChangeTable)
+ && Objects.equals(this.diffView, other.diffView)
+ && Objects.equals(this.sizeBarInChangeTable, other.sizeBarInChangeTable)
+ && Objects.equals(this.legacycidInChangeTable, other.legacycidInChangeTable)
+ && Objects.equals(this.muteCommonPathPrefixes, other.muteCommonPathPrefixes)
+ && Objects.equals(this.signedOffBy, other.signedOffBy)
+ && Objects.equals(this.emailStrategy, other.emailStrategy)
+ && Objects.equals(this.emailFormat, other.emailFormat)
+ && Objects.equals(this.defaultBaseForMerges, other.defaultBaseForMerges)
+ && Objects.equals(this.publishCommentsOnPush, other.publishCommentsOnPush)
+ && Objects.equals(this.disableKeyboardShortcuts, other.disableKeyboardShortcuts)
+ && Objects.equals(this.disableTokenHighlighting, other.disableTokenHighlighting)
+ && Objects.equals(this.workInProgressByDefault, other.workInProgressByDefault)
+ && Objects.equals(this.my, other.my)
+ && Objects.equals(this.changeTable, other.changeTable)
+ && Objects.equals(this.allowBrowserNotifications, other.allowBrowserNotifications);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ changesPerPage,
+ downloadScheme,
+ theme,
+ dateFormat,
+ timeFormat,
+ expandInlineDiffs,
+ relativeDateInChangeTable,
+ diffView,
+ sizeBarInChangeTable,
+ legacycidInChangeTable,
+ muteCommonPathPrefixes,
+ signedOffBy,
+ emailStrategy,
+ emailFormat,
+ defaultBaseForMerges,
+ publishCommentsOnPush,
+ disableKeyboardShortcuts,
+ disableTokenHighlighting,
+ workInProgressByDefault,
+ my,
+ changeTable,
+ allowBrowserNotifications);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper("GeneralPreferencesInfo")
+ .add("changesPerPage", changesPerPage)
+ .add("downloadScheme", downloadScheme)
+ .add("theme", theme)
+ .add("dateFormat", dateFormat)
+ .add("timeFormat", timeFormat)
+ .add("expandInlineDiffs", expandInlineDiffs)
+ .add("relativeDateInChangeTable", relativeDateInChangeTable)
+ .add("diffView", diffView)
+ .add("sizeBarInChangeTable", sizeBarInChangeTable)
+ .add("legacycidInChangeTable", legacycidInChangeTable)
+ .add("muteCommonPathPrefixes", muteCommonPathPrefixes)
+ .add("signedOffBy", signedOffBy)
+ .add("emailStrategy", emailStrategy)
+ .add("emailFormat", emailFormat)
+ .add("defaultBaseForMerges", defaultBaseForMerges)
+ .add("publishCommentsOnPush", publishCommentsOnPush)
+ .add("disableKeyboardShortcuts", disableKeyboardShortcuts)
+ .add("disableTokenHighlighting", disableTokenHighlighting)
+ .add("workInProgressByDefault", workInProgressByDefault)
+ .add("my", my)
+ .add("changeTable", changeTable)
+ .add("allowBrowserNotifications", allowBrowserNotifications)
+ .toString();
+ }
+
public static GeneralPreferencesInfo defaults() {
GeneralPreferencesInfo p = new GeneralPreferencesInfo();
p.changesPerPage = DEFAULT_PAGESIZE;
diff --git a/java/com/google/gerrit/extensions/common/MergePatchSetInput.java b/java/com/google/gerrit/extensions/common/MergePatchSetInput.java
index 734d7e9..4a769dd 100644
--- a/java/com/google/gerrit/extensions/common/MergePatchSetInput.java
+++ b/java/com/google/gerrit/extensions/common/MergePatchSetInput.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.common;
import com.google.gerrit.extensions.api.accounts.AccountInput;
+import java.util.Map;
public class MergePatchSetInput {
public String subject;
@@ -22,4 +23,5 @@
public String baseChange;
public MergeInput merge;
public AccountInput author;
+ public Map<String, String> validationOptions;
}
diff --git a/java/com/google/gerrit/extensions/common/ProjectInfo.java b/java/com/google/gerrit/extensions/common/ProjectInfo.java
index 46b2599..2b00710 100644
--- a/java/com/google/gerrit/extensions/common/ProjectInfo.java
+++ b/java/com/google/gerrit/extensions/common/ProjectInfo.java
@@ -27,4 +27,10 @@
public Map<String, String> branches;
public List<WebLinkInfo> webLinks;
public Map<String, LabelTypeInfo> labels;
+
+ /**
+ * Whether the query would deliver more results if not limited. Only set on the last project that
+ * is returned as a query result.
+ */
+ public Boolean _moreProjects;
}
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index fb28d30..1d9a0af 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -27,7 +27,6 @@
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.config.Server;
-import com.google.gerrit.extensions.client.ListOption;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.json.OutputFormat;
@@ -95,8 +94,6 @@
case CHANGE:
case DIFF:
data.put(
- "defaultChangeDetailHex", ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS));
- data.put(
"changeRequestsPath",
IndexPreloadingUtil.computeChangeRequestsPath(requestedPath, page).get());
data.put("changeNum", IndexPreloadingUtil.computeChangeNum(requestedPath, page).get());
@@ -121,7 +118,6 @@
serializeObject(GSON, accountApi.getEditPreferences()));
data.put("userIsAuthenticated", true);
if (page == RequestedPage.DASHBOARD) {
- data.put("defaultDashboardHex", ListOption.toHex(IndexPreloadingUtil.DASHBOARD_OPTIONS));
data.put("dashboardQuery", IndexPreloadingUtil.computeDashboardQueryList());
}
} catch (AuthException e) {
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 36fa61b..3ada18d 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -17,12 +17,10 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.common.UsedAt.Project;
-import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.restapi.Url;
import java.net.URI;
import java.net.URISyntaxException;
@@ -83,25 +81,6 @@
NEW_USER)
.map(query -> query.replaceAll("\\$\\{user}", "self"))
.collect(toImmutableList());
- public static final ImmutableSet<ListChangesOption> DASHBOARD_OPTIONS =
- ImmutableSet.of(
- ListChangesOption.LABELS,
- ListChangesOption.DETAILED_ACCOUNTS,
- ListChangesOption.SUBMIT_REQUIREMENTS,
- ListChangesOption.STAR);
-
- public static final ImmutableSet<ListChangesOption> CHANGE_DETAIL_OPTIONS =
- ImmutableSet.of(
- ListChangesOption.ALL_COMMITS,
- ListChangesOption.ALL_REVISIONS,
- ListChangesOption.CHANGE_ACTIONS,
- ListChangesOption.DETAILED_LABELS,
- ListChangesOption.DOWNLOAD_COMMANDS,
- ListChangesOption.MESSAGES,
- ListChangesOption.SUBMITTABLE,
- ListChangesOption.WEB_LINKS,
- ListChangesOption.SKIP_DIFFSTAT,
- ListChangesOption.SUBMIT_REQUIREMENTS);
@Nullable
public static String getPath(@Nullable String requestedURL) throws URISyntaxException {
diff --git a/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java b/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
index 594415a..bdc4f65 100644
--- a/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
+++ b/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
@@ -35,7 +35,7 @@
SiteStaticDirectoryServlet(
SitePaths site,
@GerritServerConfig Config cfg,
- @Named(StaticModule.CACHE) Cache<Path, Resource> cache) {
+ @Named(StaticModuleConstants.CACHE) Cache<Path, Resource> cache) {
super(cache, cfg.getBoolean("site", "refreshHeaderFooter", true));
Path p;
try {
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 8319d9d..3c8287f 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -14,6 +14,8 @@
package com.google.gerrit.httpd.raw;
+import static com.google.gerrit.httpd.raw.StaticModuleConstants.CACHE;
+import static com.google.gerrit.httpd.raw.StaticModuleConstants.POLYGERRIT_INDEX_PATHS;
import static java.nio.file.Files.exists;
import static java.nio.file.Files.isReadable;
@@ -60,28 +62,6 @@
public class StaticModule extends ServletModule {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- public static final String CACHE = "static_content";
-
- /**
- * Paths at which we should serve the main PolyGerrit application {@code index.html}.
- *
- * <p>Supports {@code "/*"} as a trailing wildcard.
- */
- public static final ImmutableList<String> POLYGERRIT_INDEX_PATHS =
- ImmutableList.of(
- "/",
- "/c/*",
- "/id/*",
- "/p/*",
- "/q/*",
- "/x/*",
- "/admin/*",
- "/dashboard/*",
- "/profile/*",
- "/groups/self",
- "/settings/*",
- "/Documentation/q/*");
-
/**
* Paths that should be treated as static assets when serving PolyGerrit.
*
diff --git a/java/com/google/gerrit/httpd/raw/StaticModuleConstants.java b/java/com/google/gerrit/httpd/raw/StaticModuleConstants.java
new file mode 100644
index 0000000..23cffa1
--- /dev/null
+++ b/java/com/google/gerrit/httpd/raw/StaticModuleConstants.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2023 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.httpd.raw;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Various constants related to {@link StaticModule}
+ *
+ * <p>Methods of the {@link StaticModule} are not used internally in google, so moving public
+ * constants into the {@link StaticModuleConstants} allows to exclude {@link StaticModule} from the
+ * google-hosted gerrit hosts.
+ */
+public final class StaticModuleConstants {
+ public static final String CACHE = "static_content";
+
+ /**
+ * Paths at which we should serve the main PolyGerrit application {@code index.html}.
+ *
+ * <p>Supports {@code "/*"} as a trailing wildcard.
+ */
+ public static final ImmutableList<String> POLYGERRIT_INDEX_PATHS =
+ ImmutableList.of(
+ "/",
+ "/c/*",
+ "/id/*",
+ "/p/*",
+ "/q/*",
+ "/x/*",
+ "/admin/*",
+ "/dashboard/*",
+ "/profile/*",
+ "/groups/self",
+ "/settings/*",
+ "/Documentation/q/*");
+
+ private StaticModuleConstants() {};
+}
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index 870d827..3ed76ba 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -19,6 +19,7 @@
import com.google.gerrit.index.query.DataSource;
import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.IndexPredicate;
+import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import java.util.Optional;
@@ -158,12 +159,14 @@
}
/**
- * Rewriter that should be invoked on queries to this index.
+ * An Optional filter that is invoked right after the results are returned from the index, but
+ * before any post-filter predicates.
*
- * <p>The default implementation does not do anything. Should be overridden by implementation, if
- * needed.
+ * <p>The filter is invoked before any other index predicates. If the filter returns 'true', then
+ * other index predicates are evaluated. Otherwise, the result from the index is not returned to
+ * the DataSource.
*/
- default IndexRewriter<V> getIndexRewriter() {
- return (in, opts) -> in;
+ default Optional<Matchable<V>> getIndexFilter() {
+ return Optional.empty();
}
}
diff --git a/java/com/google/gerrit/index/IndexConfig.java b/java/com/google/gerrit/index/IndexConfig.java
index c21f32e..2141bf2 100644
--- a/java/com/google/gerrit/index/IndexConfig.java
+++ b/java/com/google/gerrit/index/IndexConfig.java
@@ -38,6 +38,7 @@
public static Builder fromConfig(Config cfg) {
Builder b = builder();
+ setIfPresent(cfg, "defaultLimit", b::defaultLimit);
setIfPresent(cfg, "maxLimit", b::maxLimit);
setIfPresent(cfg, "maxPages", b::maxPages);
setIfPresent(cfg, "maxTerms", b::maxTerms);
@@ -67,6 +68,7 @@
public static Builder builder() {
return new AutoValue_IndexConfig.Builder()
+ .defaultLimit(Integer.MAX_VALUE)
.maxLimit(Integer.MAX_VALUE)
.maxPages(Integer.MAX_VALUE)
.maxTerms(DEFAULT_MAX_TERMS)
@@ -79,6 +81,10 @@
@AutoValue.Builder
public abstract static class Builder {
+ public abstract Builder defaultLimit(int defaultLimit);
+
+ public abstract int defaultLimit();
+
public abstract Builder maxLimit(int maxLimit);
public abstract int maxLimit();
@@ -107,6 +113,7 @@
public IndexConfig build() {
IndexConfig cfg = autoBuild();
+ checkLimit(cfg.defaultLimit(), "defaultLimit");
checkLimit(cfg.maxLimit(), "maxLimit");
checkLimit(cfg.maxPages(), "maxPages");
checkLimit(cfg.maxTerms(), "maxTerms");
@@ -121,6 +128,12 @@
}
/**
+ * Returns default limit for index queries, if the user does not provide one. If this is not set,
+ * then the max permitted limit for each user is used, which might be much higher than intended.
+ */
+ public abstract int defaultLimit();
+
+ /**
* Returns maximum limit supported by the underlying index, or limited for performance reasons.
*/
public abstract int maxLimit();
diff --git a/java/com/google/gerrit/index/PaginationType.java b/java/com/google/gerrit/index/PaginationType.java
index e7e34fd..f18f289 100644
--- a/java/com/google/gerrit/index/PaginationType.java
+++ b/java/com/google/gerrit/index/PaginationType.java
@@ -25,5 +25,8 @@
* <p>For example, Lucene implementation uses the last doc from the previous search as
* search-after object and uses the IndexSearcher.searchAfter API to get the next set of results.
*/
- SEARCH_AFTER
+ SEARCH_AFTER,
+
+ /** Index queries are executed returning all results, without internal pagination. */
+ NONE
}
diff --git a/java/com/google/gerrit/index/QueryOptions.java b/java/com/google/gerrit/index/QueryOptions.java
index 91c8d1a..29ab6d0 100644
--- a/java/com/google/gerrit/index/QueryOptions.java
+++ b/java/com/google/gerrit/index/QueryOptions.java
@@ -73,7 +73,10 @@
int backendLimit = config().maxLimit();
int limit = Ints.saturatedCast((long) limit() + start());
limit = Math.min(limit, backendLimit);
- int pageSize = Math.min(Ints.saturatedCast((long) pageSize() + start()), backendLimit);
+ int pageSize =
+ Math.min(
+ Math.min(Ints.saturatedCast((long) pageSize() + start()), config().maxPageSize()),
+ backendLimit);
return create(config(), 0, null, pageSize, pageSizeMultiplier(), limit, fields());
}
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index ff55546..29c920b 100644
--- a/java/com/google/gerrit/index/project/ProjectField.java
+++ b/java/com/google/gerrit/index/project/ProjectField.java
@@ -44,6 +44,9 @@
public static final IndexedField<ProjectData, String>.SearchSpec NAME_SPEC =
NAME_FIELD.exact("name");
+ public static final IndexedField<ProjectData, String>.SearchSpec PREFIX_NAME_SPEC =
+ NAME_FIELD.prefix("nameprefix");
+
public static final IndexedField<ProjectData, String> DESCRIPTION_FIELD =
IndexedField.<ProjectData>stringBuilder("Description")
.stored()
@@ -59,6 +62,13 @@
public static final IndexedField<ProjectData, String>.SearchSpec PARENT_NAME_SPEC =
PARENT_NAME_FIELD.exact("parent_name");
+ public static final IndexedField<ProjectData, String> PARENT_NAME_2_FIELD =
+ IndexedField.<ProjectData>stringBuilder("ParentName2")
+ .build(p -> p.getParent().map(parent -> parent.getProject().getName()).orElse(null));
+
+ public static final IndexedField<ProjectData, String>.SearchSpec PARENT_NAME_2_SPEC =
+ PARENT_NAME_2_FIELD.exact("parent_name2");
+
public static final IndexedField<ProjectData, Iterable<String>> NAME_PART_FIELD =
IndexedField.<ProjectData>iterableStringBuilder("NamePart")
.size(200)
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
index dac1012..6cd43db 100644
--- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
+++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -65,7 +65,21 @@
@Deprecated static final Schema<ProjectData> V5 = schema(V4);
// Upgrade Lucene to 8.x requires reindexing.
- static final Schema<ProjectData> V6 = schema(V5);
+ @Deprecated static final Schema<ProjectData> V6 = schema(V5);
+
+ @Deprecated
+ static final Schema<ProjectData> V7 =
+ new Schema.Builder<ProjectData>()
+ .add(V6)
+ .addIndexedFields(ProjectField.PARENT_NAME_2_FIELD)
+ .addSearchSpecs(ProjectField.PARENT_NAME_2_SPEC)
+ .build();
+
+ static final Schema<ProjectData> V8 =
+ new Schema.Builder<ProjectData>()
+ .add(V7)
+ .addSearchSpecs(ProjectField.PREFIX_NAME_SPEC)
+ .build();
/**
* Name of the project index to be used when contacting index backends or loading configurations.
diff --git a/java/com/google/gerrit/index/project/ProjectSubstringPredicate.java b/java/com/google/gerrit/index/project/ProjectSubstringPredicate.java
new file mode 100644
index 0000000..fb6e5ae
--- /dev/null
+++ b/java/com/google/gerrit/index/project/ProjectSubstringPredicate.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2023 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.index.project;
+
+import com.google.gerrit.index.query.PostFilterPredicate;
+import java.util.Locale;
+
+/** Predicate to match projects by a substring in the project name. */
+public class ProjectSubstringPredicate extends PostFilterPredicate<ProjectData> {
+
+ public ProjectSubstringPredicate(String fieldName, String value) {
+ super(fieldName, value);
+ }
+
+ @Override
+ public boolean match(ProjectData projectData) {
+ return projectData
+ .getProject()
+ .getName()
+ .toLowerCase(Locale.US)
+ .contains(getValue().toLowerCase(Locale.US));
+ }
+
+ @Override
+ public int getCost() {
+ return 2;
+ }
+}
diff --git a/java/com/google/gerrit/index/query/PaginatingSource.java b/java/com/google/gerrit/index/query/PaginatingSource.java
index 8431ccc6..98a0ed3 100644
--- a/java/com/google/gerrit/index/query/PaginatingSource.java
+++ b/java/com/google/gerrit/index/query/PaginatingSource.java
@@ -63,7 +63,13 @@
pageResultSize++;
}
- if (last != null && source instanceof Paginated) {
+ if (last != null
+ && source instanceof Paginated
+ // TODO: this fix is only for the stable branches and the real refactoring would be to
+ // restore the logic
+ // for the filtering in AndSource (L58 - 64) as per
+ // https://gerrit-review.googlesource.com/c/gerrit/+/345634/9
+ && !indexConfig.paginationType().equals(PaginationType.NONE)) {
// Restart source and continue if we have not filled the
// full limit the caller wants.
//
diff --git a/java/com/google/gerrit/index/query/Predicate.java b/java/com/google/gerrit/index/query/Predicate.java
index e251b00..fc9bc00 100644
--- a/java/com/google/gerrit/index/query/Predicate.java
+++ b/java/com/google/gerrit/index/query/Predicate.java
@@ -194,7 +194,7 @@
@Override
public abstract boolean equals(Object other);
- private static class Any<T> extends Predicate<T> implements Matchable<T> {
+ public static class Any<T> extends Predicate<T> implements Matchable<T> {
private static final Any<Object> INSTANCE = new Any<>();
private Any() {}
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 1c8bbc3..c1d92b0 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -25,6 +25,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.Index;
@@ -265,12 +266,15 @@
start,
initialPageSize,
pageSizeMultiplier,
- limit,
+ // Always bump limit by 1, even if this results in exceeding the permitted
+ // max for this user. The only way to see if there are more entities is to
+ // ask for one more result from the query.
+ // NOTE: This is consistent to the behaviour before the introduction of pagination.`
+ Ints.saturatedCast((long) limit + 1),
getRequestedFields());
logger.atFine().log("Query options: %s", opts);
// Apply index-specific rewrite first
- Predicate<T> pred = indexes.getSearchIndex().getIndexRewriter().rewrite(q, opts);
- pred = rewriter.rewrite(pred, opts);
+ Predicate<T> pred = rewriter.rewrite(q, opts);
if (enforceVisibility) {
pred = enforceVisibility(pred);
}
@@ -297,16 +301,20 @@
out = new ArrayList<>(cnt);
for (int i = 0; i < cnt; i++) {
+ String queryString = queryStrings != null ? queryStrings.get(i) : null;
ImmutableList<T> matchesList = matches.get(i).toList();
+ int matchCount = matchesList.size();
+ int limit = limits.get(i);
logger.atFine().log(
"Matches[%d]:\n%s",
i, lazy(() -> matchesList.stream().map(this::formatForLogging).collect(toList())));
- out.add(
- QueryResult.create(
- queryStrings != null ? queryStrings.get(i) : null,
- predicates.get(i),
- limits.get(i),
- matchesList));
+ // TODO(brohlfs): Remove this extra logging by end of Q3 2023.
+ if (limit > 500 && userProvidedLimit <= 0 && matchCount > 100 && enforceVisibility) {
+ logger.atWarning().log(
+ "%s index query without provided limit. effective limit: %d, result count: %d, query: %s",
+ schemaDef.getName(), getPermittedLimit(), matchCount, queryString);
+ }
+ out.add(QueryResult.create(queryString, predicates.get(i), limit, matchesList));
}
// Only measure successful queries that actually touched the index.
@@ -406,6 +414,8 @@
possibleLimits.add(getPermittedLimit());
if (userProvidedLimit > 0) {
possibleLimits.add(userProvidedLimit);
+ } else if (indexConfig.defaultLimit() > 0) {
+ possibleLimits.add(indexConfig.defaultLimit());
}
if (limitField != null) {
Integer limitFromPredicate = LimitPredicate.getLimit(limitField, p);
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 1d02dab..944f956 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -16,6 +16,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
@@ -50,6 +51,7 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -75,6 +77,7 @@
private final String indexName;
private final Map<K, D> indexedDocuments;
private int queryCount;
+ private List<Integer> resultsSizes;
AbstractFakeIndex(Schema<V> schema, SitePaths sitePaths, String indexName) {
this.schema = schema;
@@ -82,6 +85,7 @@
this.indexName = indexName;
this.indexedDocuments = new HashMap<>();
this.queryCount = 0;
+ this.resultsSizes = new ArrayList<Integer>();
}
@Override
@@ -119,6 +123,16 @@
return queryCount;
}
+ @VisibleForTesting
+ public void resetQueryCount() {
+ queryCount = 0;
+ }
+
+ @VisibleForTesting
+ public List<Integer> getResultsSizes() {
+ return resultsSizes;
+ }
+
@Override
public DataSource<V> getSource(Predicate<V> p, QueryOptions opts) {
List<V> results;
@@ -142,6 +156,7 @@
results = valueStream.skip(opts.start()).limit(opts.pageSize()).collect(toImmutableList());
}
queryCount++;
+ resultsSizes.add(results.size());
}
return new DataSource<>() {
@Override
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 8783593..07a071a 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -60,6 +60,33 @@
private static final String PKG = "com.google.gerrit.pgm";
public static final String NOT_ARCHIVED = "NOT_ARCHIVED";
+ // Classloader that allows to add additional jars to the classpath.
+ public static class GerritClassLoader extends URLClassLoader {
+ static {
+ ClassLoader.registerAsParallelCapable();
+ }
+
+ /**
+ * Constructs a new URLClassLoader for the given URLs.
+ *
+ * @param urls the URLs from which to load classes and resources
+ * @param parent the parent class loader for delegation
+ */
+ GerritClassLoader(URL[] urls, ClassLoader parent) {
+ super(urls, parent);
+ }
+
+ /**
+ * Appends the additional URL to the list of URLs to search for classes and resources.
+ *
+ * @param url the URL to be added to the search path of URLs
+ */
+ @Override
+ public void addURL(URL url) {
+ super.addURL(url);
+ }
+ }
+
private static ClassLoader daemonClassLoader;
public static void main(String[] argv) throws Exception {
@@ -308,7 +335,7 @@
if (!extapi.isEmpty()) {
parent = URLClassLoader.newInstance(extapi.toArray(new URL[extapi.size()]), parent);
}
- return URLClassLoader.newInstance(jars.values().toArray(new URL[jars.size()]), parent);
+ return new GerritClassLoader(jars.values().toArray(new URL[jars.size()]), parent);
}
private static void extractJar(ZipFile zf, ZipEntry ze, NavigableMap<String, URL> jars)
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 78ee128..938cd67 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -25,6 +25,7 @@
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -35,6 +36,7 @@
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.FieldType;
import com.google.gerrit.index.Index;
+import com.google.gerrit.index.PaginationType;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.Schema.Values;
@@ -422,6 +424,10 @@
return f.isStored() ? Field.Store.YES : Field.Store.NO;
}
+ static int getLimitBasedOnPaginationType(QueryOptions opts, int pagesize) {
+ return PaginationType.NONE == opts.config().paginationType() ? opts.limit() : pagesize;
+ }
+
private final class NrtFuture extends AbstractFuture<Void> {
private final long gen;
@@ -541,7 +547,9 @@
ScoreDoc scoreDoc = null;
try {
searcher = acquire();
- int realLimit = opts.start() + opts.pageSize();
+ int realLimit =
+ Ints.saturatedCast(
+ (long) getLimitBasedOnPaginationType(opts, opts.pageSize()) + opts.start());
TopFieldDocs docs =
opts.searchAfter() != null
? searcher.searchAfter((ScoreDoc) opts.searchAfter(), query, realLimit, sort, false)
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index b94e840..215276c 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -404,6 +404,7 @@
if (Integer.MAX_VALUE - opts.pageSize() < opts.start()) {
realPageSize = Integer.MAX_VALUE;
}
+ int queryLimit = AbstractLuceneIndex.getLimitBasedOnPaginationType(opts, realPageSize);
List<TopFieldDocs> hits = new ArrayList<>();
int searchAfterHitsCount = 0;
for (int i = 0; i < indexes.size(); i++) {
@@ -422,11 +423,10 @@
subIndex, Iterables.getLast(Arrays.asList(subIndexHits.scoreDocs), searchAfter));
}
} else {
- hits.add(searchers[i].search(query, realPageSize, sort));
+ hits.add(searchers[i].search(query, queryLimit, sort));
}
}
- TopDocs docs =
- TopDocs.merge(sort, realPageSize, hits.stream().toArray(TopFieldDocs[]::new));
+ TopDocs docs = TopDocs.merge(sort, queryLimit, hits.stream().toArray(TopFieldDocs[]::new));
List<Document> result = new ArrayList<>(docs.scoreDocs.length);
for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
diff --git a/java/com/google/gerrit/lucene/QueryBuilder.java b/java/com/google/gerrit/lucene/QueryBuilder.java
index 14ad528..4ff41a1 100644
--- a/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -79,6 +79,8 @@
return or(p);
} else if (p instanceof NotPredicate) {
return not(p);
+ } else if (p instanceof Predicate.Any) {
+ return new MatchAllDocsQuery();
} else if (p instanceof IndexPredicate) {
return fieldQuery((IndexPredicate<V>) p);
} else if (p instanceof PostFilterPredicate) {
diff --git a/java/com/google/gerrit/metrics/Field.java b/java/com/google/gerrit/metrics/Field.java
index 5508819..01b89cf 100644
--- a/java/com/google/gerrit/metrics/Field.java
+++ b/java/com/google/gerrit/metrics/Field.java
@@ -21,6 +21,8 @@
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* Describes a bucketing field used by a metric.
@@ -102,6 +104,21 @@
.metadataMapper(metadataMapper);
}
+ /**
+ * A dedicated field to be used with metrics based on {@link Metadata#projectName()}. It was
+ * introduced to sanitize the project name to avoid sub-metric name's collision.
+ *
+ * @param fieldName name of the field that contains a project name as value
+ * @return builder for the project name field
+ */
+ public static Field.Builder<String> ofProjectName(String fieldName) {
+ return new AutoValue_Field.Builder<String>()
+ .valueType(String.class)
+ .formatter(Field::sanitizeProjectName)
+ .name(fieldName)
+ .metadataMapper(Metadata.Builder::projectName);
+ }
+
/** Returns name of this field within the metric. */
public abstract String name();
@@ -137,4 +154,33 @@
return field;
}
}
+
+ private static final Pattern SUBMETRIC_NAME_PATTERN =
+ Pattern.compile("[a-zA-Z0-9_-]+([a-zA-Z0-9_-]+)*");
+ private static final Pattern INVALID_CHAR_PATTERN = Pattern.compile("[^\\w-]");
+ private static final String REPLACEMENT_PREFIX = "_0x";
+
+ private static String sanitizeProjectName(String projectName) {
+ if (SUBMETRIC_NAME_PATTERN.matcher(projectName).matches()
+ && !projectName.contains(REPLACEMENT_PREFIX)) {
+ return projectName;
+ }
+
+ String replacmentPrefixSanitizedName =
+ projectName.replaceAll(REPLACEMENT_PREFIX, REPLACEMENT_PREFIX + REPLACEMENT_PREFIX);
+ StringBuilder sanitizedName = new StringBuilder();
+ for (int i = 0; i < replacmentPrefixSanitizedName.length(); i++) {
+ Character c = replacmentPrefixSanitizedName.charAt(i);
+ Matcher matcher = INVALID_CHAR_PATTERN.matcher(c.toString());
+ if (matcher.matches()) {
+ sanitizedName.append(REPLACEMENT_PREFIX);
+ sanitizedName.append(Integer.toHexString(c).toUpperCase());
+ sanitizedName.append('_');
+ } else {
+ sanitizedName.append(c);
+ }
+ }
+
+ return sanitizedName.toString();
+ }
}
diff --git a/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java b/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
index 0821e86..2956a3e 100644
--- a/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
+++ b/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
@@ -370,15 +370,16 @@
}
/**
- * Ensures that the sanitized metric name doesn't contain invalid characters and
- * removes the risk of collision (between the sanitized metric names).
- * Modifications to the input metric name:
+ * Ensures that the sanitized metric name doesn't contain invalid characters and removes the risk
+ * of collision (between the sanitized metric names). Modifications to the input metric name:
+ *
* <ul>
- * <li/> leading <code>/</code> is replaced with <code>_</code>
- * <li/> doubled (or repeated more times) <code>/</code> are reduced to a single <code>/<code>
- * <li/> ending <code>/</code> is removed
- * <li/> all characters that are not <code>/a-zA-Z0-9_-</code> are replaced with <code>_0x[HEX CODE]_</code> (code is capitalized)
- * <li/> the replacement prefix <code>_0x</code> is prepended with another replacement prefix
+ * <li/>leading <code>/</code> is replaced with <code>_</code>
+ * <li/>doubled (or repeated more times) <code>/</code> are reduced to a single <code>/</code>
+ * <li/>ending <code>/</code> is removed
+ * <li/>all characters that are not <code>/a-zA-Z0-9_-</code> are replaced with <code>
+ * _0x[HEX CODE]_</code> (code is capitalized)
+ * <li/>the replacement prefix <code>_0x</code> is prepended with another replacement prefix
* </ul>
*
* @param name name of the metric to sanitize
diff --git a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
index d64bd19..8771448 100644
--- a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
@@ -20,7 +20,6 @@
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.server.logging.Metadata;
import java.util.Map;
import org.eclipse.jgit.storage.file.WindowCacheStats;
@@ -184,7 +183,7 @@
+ "having most data in the cache.")
.setGauge()
.setUnit("byte"),
- Field.ofString("repository_name", Metadata.Builder::projectName)
+ Field.ofProjectName("repository_name")
.description("The name of the repository.")
.build());
metrics.newTrigger(
diff --git a/java/com/google/gerrit/metrics/proc/ThreadMXBeanSun.java b/java/com/google/gerrit/metrics/proc/ThreadMXBeanSun.java
index ca750cd..998d838 100644
--- a/java/com/google/gerrit/metrics/proc/ThreadMXBeanSun.java
+++ b/java/com/google/gerrit/metrics/proc/ThreadMXBeanSun.java
@@ -21,6 +21,9 @@
ThreadMXBeanSun(java.lang.management.ThreadMXBean sys) {
this.sys = (ThreadMXBean) sys;
+ if (this.sys.isThreadAllocatedMemorySupported()) {
+ this.sys.setThreadAllocatedMemoryEnabled(true);
+ }
}
@Override
@@ -40,7 +43,7 @@
@Override
public boolean supportsAllocatedBytes() {
- return true;
+ return sys.isThreadAllocatedMemorySupported();
}
@Override
diff --git a/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
index f05187e..2bcc959 100644
--- a/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
+++ b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
@@ -19,9 +19,9 @@
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.pgm.init.api.ConsoleUI;
import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.storage.notedb.DisabledExternalIdCache;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNoteDbStorageModule;
import com.google.gerrit.server.config.GerritServerConfig;
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 79d8cec..f906b4a 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -252,6 +252,10 @@
this.replica = replica;
}
+ public boolean isReplica() {
+ return replica;
+ }
+
@VisibleForTesting
public Injector getHttpdInjector() {
return httpdInjector;
@@ -377,6 +381,7 @@
}
cfgInjector = createCfgInjector();
config = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+ config.setBoolean("container", null, "replica", replica);
indexType = IndexModule.getIndexType(cfgInjector);
sysInjector = createSysInjector();
sysInjector.getInstance(PluginGuiceEnvironment.class).setDbCfgInjector(dbInjector, cfgInjector);
diff --git a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index b7ff1f7..6967fb1 100644
--- a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -20,11 +20,11 @@
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.storage.notedb.DisabledExternalIdCache;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
diff --git a/java/com/google/gerrit/pgm/SwitchSecureStore.java b/java/com/google/gerrit/pgm/SwitchSecureStore.java
index 6dec2d8..063fcdb 100644
--- a/java/com/google/gerrit/pgm/SwitchSecureStore.java
+++ b/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -18,7 +18,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.common.JarUtil;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.SiteLibraryLoaderUtil;
import com.google.gerrit.pgm.util.SiteProgram;
@@ -79,7 +79,7 @@
return -1;
}
- IoUtil.loadJARs(newSecureStorePath);
+ JarUtil.loadJars(newSecureStorePath);
SiteLibraryLoaderUtil.loadSiteLib(sitePaths.lib_dir);
logger.atInfo().log(
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index b59b924..abaefb2 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -21,7 +21,7 @@
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Die;
-import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.common.JarUtil;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.IndexType;
@@ -331,7 +331,7 @@
"%s has more that one implementation of %s interface",
secureStore, SecureStore.class.getName()));
}
- IoUtil.loadJARs(secureStoreLib);
+ JarUtil.loadJars(secureStoreLib);
return new SecureStoreInitData(secureStoreLib, secureStores.get(0));
} catch (IOException e) {
throw new InvalidSecureStoreException(String.format("%s is not a valid jar", secureStore), e);
diff --git a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
index 35892f2..a056a08 100644
--- a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
@@ -18,8 +18,8 @@
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.SitePaths;
@@ -41,7 +41,7 @@
private final InitFlags flags;
private final SitePaths site;
private final AllUsersName allUsers;
- private final ExternalIdFactory externalIdFactory;
+ private final ExternalIdFactoryNoteDbImpl externalIdFactory;
private final AuthConfig authConfig;
@Inject
@@ -49,7 +49,7 @@
InitFlags flags,
SitePaths site,
AllUsersNameOnInitProvider allUsers,
- ExternalIdFactory externalIdFactory,
+ ExternalIdFactoryNoteDbImpl externalIdFactory,
AuthConfig authConfig) {
this.flags = flags;
this.site = site;
diff --git a/java/com/google/gerrit/pgm/init/InitModule.java b/java/com/google/gerrit/pgm/init/InitModule.java
index b4e427b..473e3aa 100644
--- a/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/java/com/google/gerrit/pgm/init/InitModule.java
@@ -17,7 +17,10 @@
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.pgm.init.api.InitStep;
import com.google.gerrit.pgm.init.api.Section;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Singleton;
import com.google.inject.binder.LinkedBindingBuilder;
import com.google.inject.internal.UniqueAnnotations;
import java.lang.annotation.Annotation;
@@ -57,6 +60,7 @@
step().to(InitDev.class);
bind(AccountsOnInit.class).to(AccountsOnInitNoteDbImpl.class);
+ bind(ExternalIdFactory.class).to(ExternalIdFactoryNoteDbImpl.class).in(Singleton.class);
}
protected LinkedBindingBuilder<InitStep> step() {
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 7aebb46..f42de84 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -27,6 +27,7 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.ChangeDraftUpdate;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.DefaultRefLogIdentityProvider;
import com.google.gerrit.server.IdentifiedUser;
@@ -42,7 +43,7 @@
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.account.ServiceUserClassifierImpl;
-import com.google.gerrit.server.account.externalids.ExternalIdCacheModule;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheModule;
import com.google.gerrit.server.cache.CacheRemovalListener;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -71,6 +72,7 @@
import com.google.gerrit.server.git.PureRevertCache;
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.notedb.ChangeDraftNotesUpdate;
import com.google.gerrit.server.notedb.NoteDbModule;
import com.google.gerrit.server.patch.DiffExecutorModule;
import com.google.gerrit.server.patch.DiffOperationsImpl;
@@ -203,6 +205,7 @@
factory(DistinctVotersPredicate.Factory.class);
factory(HasSubmoduleUpdatePredicate.Factory.class);
factory(ProjectState.Factory.class);
+ bind(ChangeDraftUpdate.ChangeDraftUpdateFactory.class).to(ChangeDraftNotesUpdate.Factory.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class);
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 6e7ac6b..3587342 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -101,6 +101,7 @@
"//lib:juniversalchardet",
"//lib:mime-util",
"//lib:protobuf",
+ "//lib:roaringbitmap",
"//lib:servlet-api",
"//lib:soy",
"//lib:tukaani-xz",
diff --git a/java/com/google/gerrit/server/ChangeDraftUpdate.java b/java/com/google/gerrit/server/ChangeDraftUpdate.java
new file mode 100644
index 0000000..eb33fb5
--- /dev/null
+++ b/java/com/google/gerrit/server/ChangeDraftUpdate.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2014 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;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import java.time.Instant;
+import java.util.List;
+import org.eclipse.jgit.lib.PersonIdent;
+
+/** An interface for updating draft comments. */
+public interface ChangeDraftUpdate {
+
+ interface ChangeDraftUpdateFactory {
+ ChangeDraftUpdate create(
+ ChangeNotes notes,
+ Account.Id accountId,
+ Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Instant when);
+
+ ChangeDraftUpdate create(
+ Change change,
+ Account.Id accountId,
+ Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Instant when);
+ }
+
+ /** Creates a draft comment. */
+ void putDraftComment(HumanComment c);
+
+ /**
+ * Marks a comment for deletion. Called when the comment is deleted because the user published it.
+ *
+ * <p>NOTE for implementers: The actual deletion of a published draft should only happen after the
+ * published comment is successfully updated. For more context, see {@link
+ * com.google.gerrit.server.notedb.NoteDbUpdateManager#execute(boolean)}.
+ *
+ * <p>TODO(nitzan) - add generalized support for the above sync issue. The implementation should
+ * support deletion of published drafts from multiple ChangeDraftUpdateFactory instances.
+ */
+ void markDraftCommentAsPublished(HumanComment c);
+
+ /**
+ * Marks a comment for deletion. Called when the comment is deleted because the user removed it.
+ */
+ void addDraftCommentForDeletion(HumanComment c);
+
+ /**
+ * Marks all comments for deletion. Called when there are inconsistencies between the published
+ * comments storage and the drafts one.
+ */
+ void addAllDraftCommentsForDeletion(List<Comment> comments);
+}
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index 2265055..dd86f88 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -22,10 +22,13 @@
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.util.CommitMessageUtil;
+import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.security.SecureRandom;
@@ -38,6 +41,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -54,6 +58,16 @@
public static final Ordering<PatchSet> PS_ID_ORDER =
Ordering.from(comparingInt(PatchSet::number));
+ private final DynamicItem<UrlFormatter> urlFormatter;
+ private final boolean enableLinkChangeIdFooters;
+
+ @Inject
+ ChangeUtil(DynamicItem<UrlFormatter> urlFormatter, @GerritServerConfig Config config) {
+ this.urlFormatter = urlFormatter;
+ this.enableLinkChangeIdFooters =
+ config.getBoolean("receive", "enableChangeIdLinkFooters", true);
+ }
+
/** Returns a new unique identifier for change message entities. */
public static String messageUuid() {
byte[] buf = new byte[8];
@@ -124,11 +138,8 @@
* @throws ResourceConflictException if the new commit message has a missing or invalid Change-Id
* @throws BadRequestException if the new commit message is null or empty
*/
- public static void ensureChangeIdIsCorrect(
- boolean requireChangeId,
- String currentChangeId,
- String newCommitMessage,
- UrlFormatter urlFormatter)
+ public void ensureChangeIdIsCorrect(
+ boolean requireChangeId, String currentChangeId, String newCommitMessage)
throws ResourceConflictException, BadRequestException {
RevCommit revCommit =
RevCommit.parse(
@@ -137,7 +148,7 @@
// Check that the commit message without footers is not empty
CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage());
- List<String> changeIdFooters = getChangeIdsFromFooter(revCommit, urlFormatter);
+ List<String> changeIdFooters = getChangeIdsFromFooter(revCommit);
if (requireChangeId && changeIdFooters.isEmpty()) {
throw new ResourceConflictException("missing Change-Id footer");
}
@@ -155,9 +166,13 @@
private static final Pattern LINK_CHANGE_ID_PATTERN = Pattern.compile("I[0-9a-f]{40}");
- public static List<String> getChangeIdsFromFooter(RevCommit c, UrlFormatter urlFormatter) {
+ public List<String> getChangeIdsFromFooter(RevCommit c) {
List<String> changeIds = c.getFooterLines(FooterConstants.CHANGE_ID);
- Optional<String> webUrl = urlFormatter.getWebUrl();
+ if (!enableLinkChangeIdFooters) {
+ return changeIds;
+ }
+
+ Optional<String> webUrl = urlFormatter.get().getWebUrl();
if (!webUrl.isPresent()) {
return changeIds;
}
@@ -176,6 +191,4 @@
return changeIds;
}
-
- private ChangeUtil() {}
}
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 285657e..957dca9 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -24,7 +24,6 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
@@ -32,12 +31,10 @@
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -53,13 +50,10 @@
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -116,18 +110,15 @@
private final DiffOperations diffOperations;
private final GitRepositoryManager repoManager;
- private final AllUsersName allUsers;
private final String serverId;
@Inject
CommentsUtil(
DiffOperations diffOperations,
GitRepositoryManager repoManager,
- AllUsersName allUsers,
@GerritServerId String serverId) {
this.diffOperations = diffOperations;
this.repoManager = repoManager;
- this.allUsers = allUsers;
this.serverId = serverId;
}
@@ -203,12 +194,6 @@
.findFirst();
}
- public Optional<HumanComment> getDraft(ChangeNotes notes, IdentifiedUser user, Comment.Key key) {
- return draftByChangeAuthor(notes, user.getAccountId()).stream()
- .filter(c -> key.equals(c.key))
- .findFirst();
- }
-
public List<HumanComment> publishedHumanCommentsByChange(ChangeNotes notes) {
notes.load();
return sort(Lists.newArrayList(notes.getHumanComments().values()));
@@ -223,30 +208,6 @@
return robotCommentsByChange(notes).stream().filter(c -> c.key.uuid.equals(uuid)).findFirst();
}
- public List<HumanComment> draftByChange(ChangeNotes notes) {
- List<HumanComment> comments = new ArrayList<>();
- for (Ref ref : getDraftRefs(notes.getChangeId())) {
- Account.Id account = Account.Id.fromRefSuffix(ref.getName());
- if (account != null) {
- comments.addAll(draftByChangeAuthor(notes, account));
- }
- }
- return sort(comments);
- }
-
- public List<HumanComment> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
- List<HumanComment> comments = new ArrayList<>();
- comments.addAll(publishedByPatchSet(notes, psId));
-
- for (Ref ref : getDraftRefs(notes.getChangeId())) {
- Account.Id account = Account.Id.fromRefSuffix(ref.getName());
- if (account != null) {
- comments.addAll(draftByPatchSetAuthor(psId, account, notes));
- }
- }
- return sort(comments);
- }
-
public List<HumanComment> publishedByChangeFile(ChangeNotes notes, String file) {
return commentsOnFile(notes.load().getHumanComments().values(), file);
}
@@ -322,22 +283,6 @@
.collect(toList());
}
- public List<HumanComment> draftByPatchSetAuthor(
- PatchSet.Id psId, Account.Id author, ChangeNotes notes) {
- return commentsOnPatchSet(notes.load().getDraftComments(author).values(), psId);
- }
-
- public List<HumanComment> draftByChangeFileAuthor(
- ChangeNotes notes, String file, Account.Id author) {
- return commentsOnFile(notes.load().getDraftComments(author).values(), file);
- }
-
- public List<HumanComment> draftByChangeAuthor(ChangeNotes notes, Account.Id author) {
- List<HumanComment> comments = new ArrayList<>();
- comments.addAll(notes.getDraftComments(author).values());
- return sort(comments);
- }
-
public void putHumanComments(
ChangeUpdate update, Comment.Status status, Iterable<HumanComment> comments) {
for (HumanComment c : comments) {
@@ -451,54 +396,7 @@
}
}
- /**
- * Get NoteDb draft refs for a change.
- *
- * <p>This is just a simple ref scan, so the results may potentially include refs for zombie draft
- * comments. A zombie draft is one which has been published but the write to delete the draft ref
- * from All-Users failed.
- *
- * @param changeId change ID.
- * @return raw refs from All-Users repo.
- */
- public Collection<Ref> getDraftRefs(Change.Id changeId) {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- return getDraftRefs(repo, changeId);
- } catch (IOException e) {
- throw new StorageException(e);
- }
- }
-
- /** returns all changes that contain draft comments of {@code accountId}. */
- public Collection<Change.Id> getChangesWithDrafts(Account.Id accountId) {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- return getChangesWithDrafts(repo, accountId);
- } catch (IOException e) {
- throw new StorageException(e);
- }
- }
-
- private Collection<Ref> getDraftRefs(Repository repo, Change.Id changeId) throws IOException {
- return repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(changeId));
- }
-
- private Collection<Change.Id> getChangesWithDrafts(Repository repo, Account.Id accountId)
- throws IOException {
- Set<Change.Id> changes = new HashSet<>();
- for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_DRAFT_COMMENTS)) {
- Integer accountIdFromRef = RefNames.parseRefSuffix(ref.getName());
- if (accountIdFromRef != null && accountIdFromRef == accountId.get()) {
- Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
- if (changeId == null) {
- continue;
- }
- changes.add(changeId);
- }
- }
- return changes;
- }
-
- private static <T extends Comment> List<T> sort(List<T> comments) {
+ public static <T extends Comment> List<T> sort(List<T> comments) {
comments.sort(COMMENT_ORDER);
return comments;
}
diff --git a/java/com/google/gerrit/server/DeleteZombieComments.java b/java/com/google/gerrit/server/DeleteZombieComments.java
new file mode 100644
index 0000000..8deee6f
--- /dev/null
+++ b/java/com/google/gerrit/server/DeleteZombieComments.java
@@ -0,0 +1,301 @@
+// Copyright (C) 2023 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;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * This class can be used to clean zombie draft comments. More context in <a
+ * href="https://gerrit-review.googlesource.com/c/gerrit/+/246233">
+ * https://gerrit-review.googlesource.com/c/gerrit/+/246233 </a>
+ *
+ * <p>The implementation has two cases for detecting zombie drafts:
+ *
+ * <ul>
+ * <li>An earlier bug in the deletion of draft comments caused some draft refs to remain empty but
+ * not get deleted.
+ * <li>Inspecting all draft-comments. Check for each draft if there exists a published comment
+ * with the same UUID. These comments are called zombie drafts. If the program is run in
+ * {@link DeleteZombieComments#dryRun} mode, the zombie draft IDs will only be logged for
+ * tracking, otherwise they will also be deleted.
+ * </uL>
+ */
+public abstract class DeleteZombieComments<KeyT> implements AutoCloseable {
+ @AutoValue
+ abstract static class ChangeUserIDsPair {
+ abstract Change.Id changeId();
+
+ abstract Account.Id accountId();
+
+ static ChangeUserIDsPair create(Change.Id changeId, Account.Id accountId) {
+ return new AutoValue_DeleteZombieComments_ChangeUserIDsPair(changeId, accountId);
+ }
+ }
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final int cleanupPercentage;
+ protected final boolean dryRun;
+ @Nullable private final Consumer<String> uiConsumer;
+ @Nullable private final GitRepositoryManager repoManager;
+ @Nullable private final DraftCommentsReader draftCommentsReader;
+ @Nullable private final ChangeNotes.Factory changeNotesFactory;
+ @Nullable private final CommentsUtil commentsUtil;
+
+ private Map<Change.Id, Project.NameKey> changeProjectMap = new HashMap<>();
+ private Map<Change.Id, ChangeNotes> changeNotes = new HashMap<>();
+
+ protected DeleteZombieComments(
+ Integer cleanupPercentage,
+ boolean dryRun,
+ Consumer<String> uiConsumer,
+ GitRepositoryManager repoManager,
+ DraftCommentsReader draftCommentsReader,
+ ChangeNotes.Factory changeNotesFactory,
+ CommentsUtil commentsUtil) {
+ this.cleanupPercentage = cleanupPercentage == null ? 100 : cleanupPercentage;
+ this.dryRun = dryRun;
+ this.uiConsumer = uiConsumer;
+ this.repoManager = repoManager;
+ this.draftCommentsReader = draftCommentsReader;
+ this.changeNotesFactory = changeNotesFactory;
+ this.commentsUtil = commentsUtil;
+ }
+
+ /** Deletes all draft comments. Returns the number of zombie draft comments that were deleted. */
+ @CanIgnoreReturnValue
+ public int execute() throws IOException {
+ setup();
+ List<KeyT> emptyDrafts = filterByCleanupPercentage(listEmptyDrafts(), "empty");
+ ListMultimap<KeyT, HumanComment> alreadyPublished = listDraftCommentsThatAreAlsoPublished();
+ if (dryRun) {
+ logInfo(
+ String.format(
+ "Running in dry run mode. Skipping deletion."
+ + "\nStats (with %d cleanup-percentage):"
+ + "\nEmpty drafts = %d"
+ + "\nAlready published drafts (zombies) = %d",
+ cleanupPercentage, emptyDrafts.size(), alreadyPublished.size()));
+ } else {
+ deleteEmptyDraftsByKey(emptyDrafts);
+ deleteZombieDrafts(alreadyPublished);
+ }
+ return emptyDrafts.size() + alreadyPublished.size();
+ }
+
+ @VisibleForTesting
+ public abstract void setup() throws IOException;
+
+ @Override
+ public abstract void close() throws IOException;
+
+ protected abstract List<KeyT> listAllDrafts() throws IOException;
+
+ protected abstract List<KeyT> listEmptyDrafts() throws IOException;
+
+ protected abstract void deleteEmptyDraftsByKey(Collection<KeyT> keys) throws IOException;
+
+ protected abstract void deleteZombieDrafts(ListMultimap<KeyT, HumanComment> drafts)
+ throws IOException;
+
+ protected abstract Change.Id getChangeId(KeyT key);
+
+ protected abstract Account.Id getAccountId(KeyT key);
+
+ protected abstract String loggable(KeyT key);
+
+ protected ChangeNotes getChangeNotes(Change.Id changeId) {
+ if (changeNotes.containsKey(changeId)) {
+ return changeNotes.get(changeId);
+ }
+ checkState(
+ changeProjectMap.containsKey(changeId),
+ "Cannot get a project associated with change ID " + changeId);
+ ChangeNotes notes = changeNotesFactory.createChecked(changeProjectMap.get(changeId), changeId);
+ changeNotes.put(changeId, notes);
+ return notes;
+ }
+
+ private List<KeyT> filterByCleanupPercentage(List<KeyT> drafts, String reason) {
+ if (cleanupPercentage >= 100) {
+ logInfo(
+ String.format(
+ "Cleanup percentage = %d" + "\nNumber of drafts to be cleaned for %s = %d",
+ cleanupPercentage, reason, drafts.size()));
+ return drafts;
+ }
+ ImmutableList<KeyT> res =
+ drafts.stream()
+ .filter(key -> getChangeId(key).get() % 100 < cleanupPercentage)
+ .collect(toImmutableList());
+ logInfo(
+ String.format(
+ "Cleanup percentage = %d"
+ + "\nOriginal number of drafts for %s = %d"
+ + "\nNumber of drafts to be processed for %s = %d",
+ cleanupPercentage, reason, drafts.size(), reason, res.size()));
+ return res;
+ }
+
+ @VisibleForTesting
+ public ListMultimap<KeyT, HumanComment> listDraftCommentsThatAreAlsoPublished()
+ throws IOException {
+ List<KeyT> draftKeys = filterByCleanupPercentage(listAllDrafts(), "all-drafts");
+ changeProjectMap.putAll(mapChangesWithDraftsToProjects(draftKeys));
+
+ ListMultimap<KeyT, HumanComment> zombieDrafts = ArrayListMultimap.create();
+ Set<ChangeUserIDsPair> visitedSet = new HashSet<>();
+ for (KeyT key : draftKeys) {
+ try {
+ Change.Id changeId = getChangeId(key);
+ Account.Id accountId = getAccountId(key);
+ ChangeUserIDsPair changeUserIDsPair = ChangeUserIDsPair.create(changeId, accountId);
+ if (!visitedSet.add(changeUserIDsPair)) {
+ continue;
+ }
+ if (!changeProjectMap.containsKey(changeId)) {
+ logger.atWarning().log(
+ "Could not find a project associated with change ID %s. Skipping draft [%s]",
+ changeId, loggable(key));
+ continue;
+ }
+ List<HumanComment> drafts =
+ draftCommentsReader.getDraftsByChangeAndDraftAuthor(changeId, accountId);
+ ChangeNotes notes = getChangeNotes(changeId);
+ List<HumanComment> published = commentsUtil.publishedHumanCommentsByChange(notes);
+ Set<String> publishedIds = toUuid(published);
+ ImmutableList<HumanComment> zombieDraftsForChangeAndAuthor =
+ drafts.stream()
+ .filter(draft -> publishedIds.contains(draft.key.uuid))
+ .collect(toImmutableList());
+ zombieDraftsForChangeAndAuthor.forEach(
+ zombieDraft ->
+ logger.atWarning().log(
+ "Draft comment with uuid '%s' of change %s, account %s, written on %s,"
+ + " is a zombie draft that is already published.",
+ zombieDraft.key.uuid, changeId, accountId, zombieDraft.writtenOn));
+ zombieDrafts.putAll(key, zombieDraftsForChangeAndAuthor);
+ } catch (RuntimeException e) {
+ logger.atWarning().withCause(e).log("Failed to process draft [%s]", loggable(key));
+ }
+ }
+
+ if (!zombieDrafts.isEmpty()) {
+ Timestamp earliestZombieTs = null;
+ Timestamp latestZombieTs = null;
+ for (HumanComment zombieDraft : zombieDrafts.values()) {
+ earliestZombieTs = getEarlierTs(earliestZombieTs, zombieDraft.writtenOn);
+ latestZombieTs = getLaterTs(latestZombieTs, zombieDraft.writtenOn);
+ }
+ logger.atWarning().log(
+ "Detected %d zombie drafts that were already published (earliest at %s, latest at %s).",
+ zombieDrafts.size(), earliestZombieTs, latestZombieTs);
+ }
+ return zombieDrafts;
+ }
+
+ /**
+ * Map each change ID to its associated project.
+ *
+ * <p>When doing a ref scan of draft refs
+ * "refs/draft-comments/$change_id_short/$change_id/$user_id" we don't know which project this
+ * draft comment is associated with. The project name is needed to load published comments for the
+ * change, hence we map each change ID to its project here by scanning through the change meta ref
+ * of the change ID in all projects.
+ */
+ private Map<Change.Id, Project.NameKey> mapChangesWithDraftsToProjects(List<KeyT> drafts) {
+ ImmutableSet<Change.Id> changeIds =
+ drafts.stream().map(key -> getChangeId(key)).collect(ImmutableSet.toImmutableSet());
+ Map<Change.Id, Project.NameKey> result = new HashMap<>();
+ for (Project.NameKey project : repoManager.list()) {
+ try (Repository repo = repoManager.openRepository(project)) {
+ Sets.SetView<Change.Id> unmappedChangeIds = Sets.difference(changeIds, result.keySet());
+ for (Change.Id changeId : unmappedChangeIds) {
+ Ref ref = repo.getRefDatabase().exactRef(RefNames.changeMetaRef(changeId));
+ if (ref != null) {
+ result.put(changeId, project);
+ }
+ }
+ } catch (Exception e) {
+ logger.atWarning().withCause(e).log("Failed to open repository for project '%s'.", project);
+ }
+ if (changeIds.size() == result.size()) {
+ // We do not need to scan the remaining repositories
+ break;
+ }
+ }
+ if (result.size() != changeIds.size()) {
+ logger.atWarning().log(
+ "Failed to associate the following change Ids to a project: %s",
+ Sets.difference(changeIds, result.keySet()));
+ }
+ return result;
+ }
+
+ protected void logInfo(String message) {
+ logger.atInfo().log("%s", message);
+ uiConsumer.accept(message);
+ }
+
+ /** Map the list of input comments to their UUIDs. */
+ private Set<String> toUuid(List<HumanComment> in) {
+ return in.stream().map(c -> c.key.uuid).collect(toImmutableSet());
+ }
+
+ private Timestamp getEarlierTs(@Nullable Timestamp t1, Timestamp t2) {
+ if (t1 == null) {
+ return t2;
+ }
+ return t1.before(t2) ? t1 : t2;
+ }
+
+ private Timestamp getLaterTs(@Nullable Timestamp t1, Timestamp t2) {
+ if (t1 == null) {
+ return t2;
+ }
+ return t1.after(t2) ? t1 : t2;
+ }
+}
diff --git a/java/com/google/gerrit/server/DraftCommentsReader.java b/java/com/google/gerrit/server/DraftCommentsReader.java
new file mode 100644
index 0000000..1eea228
--- /dev/null
+++ b/java/com/google/gerrit/server/DraftCommentsReader.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2023 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;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+public interface DraftCommentsReader {
+ /**
+ * Returns a single draft of the provided change, that was written by {@code author} and has the
+ * given {@code key}, or {@code Optional::empty} if there is no such comment.
+ */
+ Optional<HumanComment> getDraftComment(ChangeNotes notes, IdentifiedUser author, Comment.Key key);
+
+ /**
+ * Returns all drafts of the provided change, that were written by {@code author}. The comments
+ * are sorted by {@link CommentsUtil#COMMENT_ORDER}.
+ */
+ List<HumanComment> getDraftsByChangeAndDraftAuthor(ChangeNotes notes, Account.Id author);
+
+ /**
+ * Returns all drafts of the provided change, that were written by {@code author}. The comments
+ * are sorted by {@link CommentsUtil#COMMENT_ORDER}.
+ *
+ * <p>If you already have a ChangeNotes instance, consider using {@link
+ * #getDraftsByChangeAndDraftAuthor(ChangeNotes, Account.Id)} instead.
+ */
+ List<HumanComment> getDraftsByChangeAndDraftAuthor(Change.Id changeId, Account.Id author);
+
+ /**
+ * Returns all drafts of the provided patch set, that were written by {@code author}. The comments
+ * are sorted by {@link CommentsUtil#COMMENT_ORDER}.
+ */
+ List<HumanComment> getDraftsByPatchSetAndDraftAuthor(
+ ChangeNotes notes, PatchSet.Id psId, Account.Id author);
+
+ /**
+ * Returns all drafts of the provided change, regardless of the author. The comments are sorted by
+ * {@link CommentsUtil#COMMENT_ORDER}.
+ */
+ List<HumanComment> getDraftsByChangeForAllAuthors(ChangeNotes notes);
+
+ /** Returns all users that have any draft comments on the provided change. */
+ Set<Account.Id> getUsersWithDrafts(ChangeNotes changeNotes);
+
+ /** Returns all changes that contain draft comments of {@code author}. */
+ Set<Change.Id> getChangesWithDrafts(Account.Id author);
+}
diff --git a/java/com/google/gerrit/server/PublishCommentsOp.java b/java/com/google/gerrit/server/PublishCommentsOp.java
index 830928a..bfafcb6 100644
--- a/java/com/google/gerrit/server/PublishCommentsOp.java
+++ b/java/com/google/gerrit/server/PublishCommentsOp.java
@@ -48,7 +48,7 @@
private final PatchSetUtil psUtil;
private final ChangeNotes.Factory changeNotesFactory;
private final CommentAdded commentAdded;
- private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
private final EmailReviewComments.Factory email;
private final Project.NameKey projectNameKey;
private final PatchSet.Id psId;
@@ -67,7 +67,7 @@
public PublishCommentsOp(
ChangeNotes.Factory changeNotesFactory,
CommentAdded commentAdded,
- CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
EmailReviewComments.Factory email,
PatchSetUtil psUtil,
PublishCommentUtil publishCommentUtil,
@@ -76,7 +76,7 @@
@Assisted Project.NameKey projectNameKey) {
this.changeNotesFactory = changeNotesFactory;
this.commentAdded = commentAdded;
- this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.email = email;
this.psId = psId;
this.publishCommentUtil = publishCommentUtil;
@@ -90,7 +90,9 @@
throws ResourceConflictException, UnprocessableEntityException, IOException,
PatchListNotAvailableException, CommentsRejectedException {
preUpdateMetaId = ctx.getNotes().getMetaId();
- comments = commentsUtil.draftByChangeAuthor(ctx.getNotes(), ctx.getUser().getAccountId());
+ comments =
+ draftCommentsReader.getDraftsByChangeAndDraftAuthor(
+ ctx.getNotes(), ctx.getUser().getAccountId());
// PublishCommentsOp should update a separate ChangeUpdate Object than the one used by other ops
// For example, with the "publish comments on PS upload" workflow,
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 8b41356..fea48ae 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -14,232 +14,34 @@
package com.google.gerrit.server;
-import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toSet;
-
-import com.google.auto.value.AutoValue;
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Joiner;
-import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSortedSet;
-import com.google.common.flogger.FluentLogger;
-import com.google.common.primitives.Ints;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.git.GitUpdateFailureException;
-import com.google.gerrit.git.LockFailureException;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-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.logging.TraceContext.TraceTimer;
-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.Collection;
-import java.util.Collections;
import java.util.List;
-import java.util.NavigableSet;
-import java.util.Objects;
import java.util.Set;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
-import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-@Singleton
-public class StarredChangesUtil {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- @AutoValue
- public abstract static class StarField {
- private static final String SEPARATOR = ":";
-
- @Nullable
- public static StarField parse(String s) {
- int p = s.indexOf(SEPARATOR);
- if (p >= 0) {
- Integer id = Ints.tryParse(s.substring(0, p));
- if (id == null) {
- return null;
- }
- Account.Id accountId = Account.id(id);
- String label = s.substring(p + 1);
- return create(accountId, label);
- }
- return null;
- }
-
- public static StarField create(Account.Id accountId, String label) {
- return new AutoValue_StarredChangesUtil_StarField(accountId, label);
- }
-
- public abstract Account.Id accountId();
-
- public abstract String label();
-
- @Override
- public final String toString() {
- return accountId() + SEPARATOR + label();
- }
- }
-
- public enum Operation {
- ADD,
- REMOVE
- }
-
- @AutoValue
- public abstract static class StarRef {
- private static final StarRef MISSING =
- new AutoValue_StarredChangesUtil_StarRef(null, Collections.emptyNavigableSet());
-
- private static StarRef create(Ref ref, Iterable<String> labels) {
- return new AutoValue_StarredChangesUtil_StarRef(
- requireNonNull(ref), ImmutableSortedSet.copyOf(labels));
- }
-
- @Nullable
- public abstract Ref ref();
-
- public abstract NavigableSet<String> labels();
-
- public ObjectId objectId() {
- return ref() != null ? ref().getObjectId() : ObjectId.zeroId();
- }
- }
-
- public static class IllegalLabelException extends Exception {
- private static final long serialVersionUID = 1L;
-
- IllegalLabelException(String message) {
- super(message);
- }
- }
-
- public static class InvalidLabelsException extends IllegalLabelException {
- private static final long serialVersionUID = 1L;
-
- InvalidLabelsException(Set<String> invalidLabels) {
- super(String.format("invalid labels: %s", Joiner.on(", ").join(invalidLabels)));
- }
- }
-
- public static class MutuallyExclusiveLabelsException extends IllegalLabelException {
- private static final long serialVersionUID = 1L;
-
- MutuallyExclusiveLabelsException(String label1, String label2) {
- super(
- String.format(
- "The labels %s and %s are mutually exclusive. Only one of them can be set.",
- label1, label2));
- }
- }
-
- public static final String DEFAULT_LABEL = "star";
-
- private final GitRepositoryManager repoManager;
- private final GitReferenceUpdated gitRefUpdated;
- private final AllUsersName allUsers;
- private final Provider<PersonIdent> serverIdent;
-
- @Inject
- StarredChangesUtil(
- GitRepositoryManager repoManager,
- GitReferenceUpdated gitRefUpdated,
- AllUsersName allUsers,
- @GerritPersonIdent Provider<PersonIdent> serverIdent) {
- this.repoManager = repoManager;
- this.gitRefUpdated = gitRefUpdated;
- this.allUsers = allUsers;
- this.serverIdent = serverIdent;
- }
-
- public NavigableSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)).labels();
- } catch (IOException e) {
- throw new StorageException(
- String.format(
- "Reading stars from change %d for account %d failed",
- changeId.get(), accountId.get()),
- e);
- }
- }
-
- public void star(Account.Id accountId, Change.Id changeId, Operation op)
- throws IllegalLabelException {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- String refName = RefNames.refsStarredChanges(changeId, accountId);
- StarRef old = readLabels(repo, refName);
-
- NavigableSet<String> labels = new TreeSet<>(old.labels());
- switch (op) {
- case ADD:
- labels.add(DEFAULT_LABEL);
- break;
- case REMOVE:
- labels.remove(DEFAULT_LABEL);
- break;
- }
-
- if (labels.isEmpty()) {
- deleteRef(repo, refName, old.objectId());
- } else {
- updateLabels(repo, refName, old.objectId(), labels);
- }
- } catch (IOException e) {
- throw new StorageException(
- String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
- e);
- }
- }
+public interface StarredChangesUtil {
+ boolean isStarred(Account.Id accountId, Change.Id changeId);
/**
* Returns a subset of change IDs among the input {@code changeIds} list that are starred by the
* {@code caller} user.
*/
- public Set<Change.Id> areStarred(
- Repository allUsersRepo, List<Change.Id> changeIds, Account.Id caller) {
- List<String> starRefs =
- changeIds.stream()
- .map(c -> RefNames.refsStarredChanges(c, caller))
- .collect(Collectors.toList());
- try {
- return allUsersRepo.getRefDatabase().exactRef(starRefs.toArray(new String[0])).keySet()
- .stream()
- .map(r -> Change.Id.fromAllUsersRef(r))
- .collect(Collectors.toSet());
- } catch (IOException e) {
- logger.atWarning().withCause(e).log(
- "Failed getting starred changes for account %d within changes: %s",
- caller.get(), Joiner.on(", ").join(changeIds));
- return ImmutableSet.of();
- }
- }
+ Set<Change.Id> areStarred(Repository allUsersRepo, List<Change.Id> changeIds, Account.Id caller);
+
+ ImmutableMap<Account.Id, Ref> byChange(Change.Id changeId);
+
+ ImmutableSet<Change.Id> byAccountId(Account.Id accountId);
+
+ ImmutableSet<Change.Id> byAccountId(Account.Id accountId, boolean skipInvalidChanges);
+
+ void star(Account.Id accountId, Change.Id changeId);
+
+ void unstar(Account.Id accountId, Change.Id changeId);
/**
* Unstar the given change for all users.
@@ -250,239 +52,5 @@
* @param changeId change ID.
* @throws IOException if an error occurred.
*/
- public void unstarAllForChangeDeletion(Change.Id changeId) throws IOException {
- try (Repository repo = repoManager.openRepository(allUsers);
- RevWalk rw = new RevWalk(repo)) {
- BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
- batchUpdate.setAllowNonFastForwards(true);
- batchUpdate.setRefLogIdent(serverIdent.get());
- batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
- for (Account.Id accountId : getStars(repo, changeId)) {
- String refName = RefNames.refsStarredChanges(changeId, accountId);
- Ref ref = repo.getRefDatabase().exactRef(refName);
- if (ref != null) {
- batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
- }
- }
- batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
- for (ReceiveCommand command : batchUpdate.getCommands()) {
- if (command.getResult() != ReceiveCommand.Result.OK) {
- String message =
- String.format(
- "Unstar change %d failed, ref %s could not be deleted: %s",
- changeId.get(), command.getRefName(), command.getResult());
- if (command.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
- throw new LockFailureException(message, batchUpdate);
- }
- throw new GitUpdateFailureException(message, batchUpdate);
- }
- }
- }
- }
-
- public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
- for (Account.Id accountId : getStars(repo, changeId)) {
- builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
- }
- return builder.build();
- } catch (IOException e) {
- throw new StorageException(
- String.format("Get accounts that starred change %d failed", changeId.get()), e);
- }
- }
-
- public ImmutableSet<Change.Id> byAccountId(Account.Id accountId) {
- return byAccountId(accountId, true);
- }
-
- public ImmutableSet<Change.Id> byAccountId(Account.Id accountId, boolean skipInvalidChanges) {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- ImmutableSet.Builder<Change.Id> builder = ImmutableSet.builder();
- for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_STARRED_CHANGES)) {
- Account.Id currentAccountId = Account.Id.fromRef(ref.getName());
- // Skip all refs that don't correspond with accountId.
- if (currentAccountId == null || !currentAccountId.equals(accountId)) {
- continue;
- }
- // Skip all refs that don't contain the required label.
- StarRef starRef = readLabels(repo, ref.getName());
- if (!starRef.labels().contains(DEFAULT_LABEL)) {
- continue;
- }
-
- // Skip invalid change ids.
- Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
- if (skipInvalidChanges && changeId == null) {
- continue;
- }
- builder.add(changeId);
- }
- return builder.build();
- } catch (IOException e) {
- throw new StorageException(
- String.format("Get starred changes for account %d failed", accountId.get()), e);
- }
- }
-
- private static Set<Account.Id> getStars(Repository allUsers, Change.Id changeId)
- throws IOException {
- String prefix = RefNames.refsStarredChangesPrefix(changeId);
- RefDatabase refDb = allUsers.getRefDatabase();
- return refDb.getRefsByPrefix(prefix).stream()
- .map(r -> r.getName().substring(prefix.length()))
- .map(refPart -> Ints.tryParse(refPart))
- .filter(Objects::nonNull)
- .map(id -> Account.id(id))
- .collect(toSet());
- }
-
- public ObjectId getObjectId(Account.Id accountId, Change.Id changeId) {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- Ref ref = repo.exactRef(RefNames.refsStarredChanges(changeId, accountId));
- return ref != null ? ref.getObjectId() : ObjectId.zeroId();
- } catch (IOException e) {
- logger.atSevere().withCause(e).log(
- "Getting star object ID for account %d on change %d failed",
- accountId.get(), changeId.get());
- return ObjectId.zeroId();
- }
- }
-
- public static StarRef readLabels(Repository repo, String refName) throws IOException {
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- "Read star labels", Metadata.builder().noteDbRefName(refName).build())) {
- Ref ref = repo.exactRef(refName);
- return readLabels(repo, ref);
- }
- }
-
- public static StarRef readLabels(Repository repo, Ref ref) throws IOException {
- if (ref == null) {
- return StarRef.MISSING;
- }
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- String.format("Read star labels from %s (without ref lookup)", ref.getName()));
- ObjectReader reader = repo.newObjectReader()) {
- ObjectLoader obj = reader.open(ref.getObjectId(), Constants.OBJ_BLOB);
- return StarRef.create(
- ref,
- Splitter.on(CharMatcher.whitespace())
- .omitEmptyStrings()
- .split(new String(obj.getCachedBytes(Integer.MAX_VALUE), UTF_8)));
- }
- }
-
- public static ObjectId writeLabels(Repository repo, Collection<String> labels)
- throws IOException, InvalidLabelsException {
- validateLabels(labels);
- try (ObjectInserter oi = repo.newObjectInserter()) {
- ObjectId id =
- oi.insert(
- Constants.OBJ_BLOB,
- labels.stream().sorted().distinct().collect(joining("\n")).getBytes(UTF_8));
- oi.flush();
- return id;
- }
- }
-
- private static void validateLabels(Collection<String> labels) throws InvalidLabelsException {
- if (labels == null) {
- return;
- }
-
- NavigableSet<String> invalidLabels = new TreeSet<>();
- for (String label : labels) {
- if (CharMatcher.whitespace().matchesAnyOf(label)) {
- invalidLabels.add(label);
- }
- }
- if (!invalidLabels.isEmpty()) {
- throw new InvalidLabelsException(invalidLabels);
- }
- }
-
- private void updateLabels(
- Repository repo, String refName, ObjectId oldObjectId, Collection<String> labels)
- throws IOException, InvalidLabelsException {
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- "Update star labels",
- Metadata.builder().noteDbRefName(refName).resourceCount(labels.size()).build());
- RevWalk rw = new RevWalk(repo)) {
- RefUpdate u = repo.updateRef(refName);
- u.setExpectedOldObjectId(oldObjectId);
- u.setForceUpdate(true);
- u.setNewObjectId(writeLabels(repo, labels));
- u.setRefLogIdent(serverIdent.get());
- u.setRefLogMessage("Update star labels", true);
- try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
- RefUpdate.Result result = u.update(rw);
- switch (result) {
- case NEW:
- case FORCED:
- case NO_CHANGE:
- case FAST_FORWARD:
- gitRefUpdated.fire(allUsers, u, null);
- return;
- case LOCK_FAILURE:
- throw new LockFailureException(
- String.format("Update star labels on ref %s failed", refName), u);
- case IO_FAILURE:
- case NOT_ATTEMPTED:
- case REJECTED:
- case REJECTED_CURRENT_BRANCH:
- case RENAMED:
- case REJECTED_MISSING_OBJECT:
- case REJECTED_OTHER_REASON:
- default:
- throw new StorageException(
- String.format("Update star labels on ref %s failed: %s", refName, result.name()));
- }
- }
- }
- }
-
- private void deleteRef(Repository repo, String refName, ObjectId oldObjectId) throws IOException {
- if (ObjectId.zeroId().equals(oldObjectId)) {
- // ref doesn't exist
- return;
- }
-
- try (TraceTimer traceTimer =
- TraceContext.newTimer(
- "Delete star labels", Metadata.builder().noteDbRefName(refName).build())) {
- RefUpdate u = repo.updateRef(refName);
- u.setForceUpdate(true);
- u.setExpectedOldObjectId(oldObjectId);
- u.setRefLogIdent(serverIdent.get());
- u.setRefLogMessage("Unstar change", true);
- try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
- RefUpdate.Result result = u.delete();
- switch (result) {
- case FORCED:
- gitRefUpdated.fire(allUsers, u, null);
- return;
- case LOCK_FAILURE:
- throw new LockFailureException(String.format("Delete star ref %s failed", refName), u);
- case NEW:
- case NO_CHANGE:
- case FAST_FORWARD:
- case IO_FAILURE:
- case NOT_ATTEMPTED:
- case REJECTED:
- case REJECTED_CURRENT_BRANCH:
- case RENAMED:
- case REJECTED_MISSING_OBJECT:
- case REJECTED_OTHER_REASON:
- default:
- throw new StorageException(
- String.format("Delete star ref %s failed: %s", refName, result.name()));
- }
- }
- }
- }
+ void unstarAllForChangeDeletion(Change.Id changeId) throws IOException;
}
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 2020d2f..c267822 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -27,6 +27,8 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.index.Schema;
@@ -585,6 +587,13 @@
.addAll(nameOrEmailSearchers)
.build();
+ private final ImmutableList<Searcher<?>> exactSearchers =
+ ImmutableList.<Searcher<?>>builder()
+ .add(new BySelf())
+ .add(new ByExactAccountId())
+ .add(new ByEmail())
+ .build();
+
private final AccountCache accountCache;
private final AccountControl.Factory accountControlFactory;
private final Emails emails;
@@ -650,6 +659,17 @@
input, searchers, self.get(), this::currentUserCanSeePredicate, AccountResolver::isActive);
}
+ /** Resolves accounts using exact searchers. Similar to the previous method. */
+ @UsedAt(Project.GOOGLE)
+ public Result resolveExact(String input) throws ConfigInvalidException, IOException {
+ return searchImpl(
+ input,
+ exactSearchers,
+ self.get(),
+ this::currentUserCanSeePredicate,
+ AccountResolver::isActive);
+ }
+
public Result resolve(String input, Predicate<AccountState> accountActivityPredicate)
throws ConfigInvalidException, IOException {
return searchImpl(
diff --git a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
index f1cf9fe..41a02a9 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
@@ -15,20 +15,19 @@
package com.google.gerrit.server.account;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.server.git.meta.VersionedMetaData;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.FileMode;
-/** User configured named destinations. */
+/** User or Group configured named destinations. */
public class VersionedAccountDestinations extends VersionedMetaData {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- public static VersionedAccountDestinations forUser(Account.Id id) {
- return new VersionedAccountDestinations(RefNames.refsUsers(id));
+ public static VersionedAccountDestinations forBranch(BranchNameKey branch) {
+ return new VersionedAccountDestinations(branch.branch());
}
private final String ref;
diff --git a/java/com/google/gerrit/server/account/VersionedAccountQueries.java b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
index 5e63875..0269ccf 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountQueries.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -18,8 +18,7 @@
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.server.git.ValidationError;
import com.google.gerrit.server.git.meta.VersionedMetaData;
import java.io.IOException;
@@ -37,8 +36,8 @@
public class VersionedAccountQueries extends VersionedMetaData {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- public static VersionedAccountQueries forUser(Account.Id id) {
- return new VersionedAccountQueries(RefNames.refsUsers(id));
+ public static VersionedAccountQueries forBranch(BranchNameKey branch) {
+ return new VersionedAccountQueries(branch.branch());
}
private final String ref;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index ef1781e..c39f60b 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -99,10 +99,10 @@
private static final long serialVersionUID = 1L;
- static final String EXTERNAL_ID_SECTION = "externalId";
- static final String ACCOUNT_ID_KEY = "accountId";
- static final String EMAIL_KEY = "email";
- static final String PASSWORD_KEY = "password";
+ public static final String EXTERNAL_ID_SECTION = "externalId";
+ public static final String ACCOUNT_ID_KEY = "accountId";
+ public static final String EMAIL_KEY = "email";
+ public static final String PASSWORD_KEY = "password";
/**
* Scheme used to label accounts created, when using the LDAP-based authentication types {@link
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
index fe8feac..a23e7bc 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
@@ -19,7 +19,6 @@
import com.google.gerrit.entities.Account;
import java.io.IOException;
import java.util.Optional;
-import org.eclipse.jgit.lib.ObjectId;
/**
* Caches external IDs of all accounts. Note that the granularity is "revision" only, so each update
@@ -35,8 +34,6 @@
ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException;
- ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException;
-
ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException;
ImmutableSetMultimap<String, ExternalId> byEmails(String... emails) throws IOException;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
index b16f73f..d226565 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
@@ -14,33 +14,10 @@
package com.google.gerrit.server.account.externalids;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.server.account.HashedPassword;
-import com.google.gerrit.server.config.AuthConfig;
-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;
-@Singleton
-public class ExternalIdFactory {
- private final ExternalIdKeyFactory externalIdKeyFactory;
- private AuthConfig authConfig;
-
- @Inject
- public ExternalIdFactory(ExternalIdKeyFactory externalIdKeyFactory, AuthConfig authConfig) {
- this.externalIdKeyFactory = externalIdKeyFactory;
- this.authConfig = authConfig;
- }
-
+public interface ExternalIdFactory {
/**
* Creates an external ID.
*
@@ -50,9 +27,7 @@
* @param accountId the ID of the account to which the external ID belongs
* @return the created external ID
*/
- public ExternalId create(String scheme, String id, Account.Id accountId) {
- return create(externalIdKeyFactory.create(scheme, id), accountId, null, null);
- }
+ ExternalId create(String scheme, String id, Account.Id accountId);
/**
* Creates an external ID.
@@ -65,14 +40,12 @@
* @param hashedPassword the hashed password of the external ID, may be {@code null}
* @return the created external ID
*/
- public ExternalId create(
+ ExternalId create(
String scheme,
String id,
Account.Id accountId,
@Nullable String email,
- @Nullable String hashedPassword) {
- return create(externalIdKeyFactory.create(scheme, id), accountId, email, hashedPassword);
- }
+ @Nullable String hashedPassword);
/**
* Creates an external ID.
@@ -81,9 +54,7 @@
* @param accountId the ID of the account to which the external ID belongs
* @return the created external ID
*/
- public ExternalId create(ExternalId.Key key, Account.Id accountId) {
- return create(key, accountId, null, null);
- }
+ ExternalId create(ExternalId.Key key, Account.Id accountId);
/**
* Creates an external ID.
@@ -94,14 +65,11 @@
* @param hashedPassword the hashed password of the external ID, may be {@code null}
* @return the created external ID
*/
- public ExternalId create(
+ ExternalId create(
ExternalId.Key key,
Account.Id accountId,
@Nullable String email,
- @Nullable String hashedPassword) {
- return create(
- key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
- }
+ @Nullable String hashedPassword);
/**
* Creates an external ID adding a hashed password computed from a plain password.
@@ -112,16 +80,11 @@
* @param plainPassword the plain HTTP password, may be {@code null}
* @return the created external ID
*/
- public ExternalId createWithPassword(
+ ExternalId createWithPassword(
ExternalId.Key key,
Account.Id accountId,
@Nullable String email,
- @Nullable String plainPassword) {
- plainPassword = Strings.emptyToNull(plainPassword);
- String hashedPassword =
- plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
- return create(key, accountId, email, hashedPassword);
- }
+ @Nullable String plainPassword);
/**
* Create a external ID for a username (scheme "username").
@@ -131,14 +94,7 @@
* @param plainPassword the plain HTTP password, may be {@code null}
* @return the created external ID
*/
- public ExternalId createUsername(
- String id, Account.Id accountId, @Nullable String plainPassword) {
- return createWithPassword(
- externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, id),
- accountId,
- null,
- plainPassword);
- }
+ ExternalId createUsername(String id, Account.Id accountId, @Nullable String plainPassword);
/**
* Creates an external ID with an email.
@@ -150,10 +106,8 @@
* @param email the email of the external ID, may be {@code null}
* @return the created external ID
*/
- public ExternalId createWithEmail(
- String scheme, String id, Account.Id accountId, @Nullable String email) {
- return createWithEmail(externalIdKeyFactory.create(scheme, id), accountId, email);
- }
+ ExternalId createWithEmail(
+ String scheme, String id, Account.Id accountId, @Nullable String email);
/**
* Creates an external ID with an email.
@@ -163,10 +117,7 @@
* @param email the email of the external ID, may be {@code null}
* @return the created external ID
*/
- public ExternalId createWithEmail(
- ExternalId.Key key, Account.Id accountId, @Nullable String email) {
- return create(key, accountId, Strings.emptyToNull(email), null);
- }
+ ExternalId createWithEmail(ExternalId.Key key, Account.Id accountId, @Nullable String email);
/**
* Creates an external ID using the `mailto`-scheme.
@@ -175,162 +126,5 @@
* @param email the email of the external ID, may be {@code null}
* @return the created external ID
*/
- public ExternalId createEmail(Account.Id accountId, String email) {
- return createWithEmail(ExternalId.SCHEME_MAILTO, email, accountId, requireNonNull(email));
- }
-
- ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
- return create(extId.key(), extId.accountId(), extId.email(), extId.password(), blobId);
- }
-
- /**
- * Creates an external ID.
- *
- * @param key the external Id key
- * @param accountId the ID of the account to which the external ID belongs
- * @param email the email of the external ID, may be {@code null}
- * @param hashedPassword the hashed password of the external ID, may be {@code null}
- * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
- * {@code null} if the external ID was created in code and is not yet stored in Git.
- * @return the created external ID
- */
- public ExternalId create(
- ExternalId.Key key,
- Account.Id accountId,
- @Nullable String email,
- @Nullable String hashedPassword,
- @Nullable ObjectId blobId) {
- return ExternalId.create(
- key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
- }
-
- /**
- * Parses an external ID from a byte array that contains the external ID as a Git config file
- * text.
- *
- * <p>The Git config must have exactly one externalId subsection with an accountId and optionally
- * email and password:
- *
- * <pre>
- * [externalId "username:jdoe"]
- * accountId = 1003407
- * email = jdoe@example.com
- * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
- * </pre>
- *
- * @param noteId the SHA-1 sum of the external ID used as the note's ID
- * @param raw a byte array that contains the external ID as a Git config file text.
- * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
- * {@code null} if the external ID was created in code and is not yet stored in Git.
- * @return the parsed external ID
- */
- public ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
- throws ConfigInvalidException {
- requireNonNull(blobId);
-
- Config externalIdConfig = new Config();
- try {
- externalIdConfig.fromText(new String(raw, UTF_8));
- } catch (ConfigInvalidException e) {
- throw invalidConfig(noteId, e.getMessage());
- }
-
- Set<String> externalIdKeys = externalIdConfig.getSubsections(ExternalId.EXTERNAL_ID_SECTION);
- if (externalIdKeys.size() != 1) {
- throw invalidConfig(
- noteId,
- String.format(
- "Expected exactly 1 '%s' section, found %d",
- ExternalId.EXTERNAL_ID_SECTION, externalIdKeys.size()));
- }
-
- String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
- ExternalId.Key externalIdKey = externalIdKeyFactory.parse(externalIdKeyStr);
- if (externalIdKey == null) {
- throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
- }
-
- if (!externalIdKey.sha1().getName().equals(noteId)) {
- if (!authConfig.isUserNameCaseInsensitiveMigrationMode()) {
- throw invalidConfig(
- noteId,
- String.format(
- "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
- }
-
- if (!externalIdKey.caseSensitiveSha1().getName().equals(noteId)) {
- throw invalidConfig(
- noteId,
- String.format(
- "Neither case sensitive nor case insensitive SHA1 of external ID '%s' match note ID"
- + " '%s'",
- externalIdKeyStr, noteId));
- }
- externalIdKey =
- externalIdKeyFactory.create(externalIdKey.scheme(), externalIdKey.id(), false);
- }
-
- String email =
- externalIdConfig.getString(
- ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.EMAIL_KEY);
- String password =
- externalIdConfig.getString(
- ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.PASSWORD_KEY);
- int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
-
- return create(
- externalIdKey,
- Account.id(accountId),
- Strings.emptyToNull(email),
- Strings.emptyToNull(password),
- blobId);
- }
-
- private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
- throws ConfigInvalidException {
- String accountIdStr =
- externalIdConfig.getString(
- ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY);
- if (accountIdStr == null) {
- throw invalidConfig(
- noteId,
- String.format(
- "Value for '%s.%s.%s' is missing, expected account ID",
- ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY));
- }
-
- try {
- int accountId =
- externalIdConfig.getInt(
- ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY, -1);
- if (accountId < 0) {
- throw invalidConfig(
- noteId,
- String.format(
- "Value %s for '%s.%s.%s' is invalid, expected account ID",
- accountIdStr,
- ExternalId.EXTERNAL_ID_SECTION,
- externalIdKeyStr,
- ExternalId.ACCOUNT_ID_KEY));
- }
- return accountId;
- } catch (IllegalArgumentException e) {
- ConfigInvalidException newException =
- invalidConfig(
- noteId,
- String.format(
- "Value %s for '%s.%s.%s' is invalid, expected account ID",
- accountIdStr,
- ExternalId.EXTERNAL_ID_SECTION,
- externalIdKeyStr,
- ExternalId.ACCOUNT_ID_KEY));
- newException.initCause(e);
- throw newException;
- }
- }
-
- private static ConfigInvalidException invalidConfig(String noteId, String message) {
- return new ConfigInvalidException(
- String.format("Invalid external ID config for note '%s': %s", noteId, message));
- }
+ ExternalId createEmail(Account.Id accountId, String email);
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
index da7b357..2d3e241 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
@@ -19,7 +19,6 @@
public class ExternalIdModule extends AbstractModule {
@Override
protected void configure() {
- bind(ExternalIdFactory.class);
bind(ExternalIdKeyFactory.class);
bind(PasswordVerifier.class);
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdUpsertPreprocessor.java b/java/com/google/gerrit/server/account/externalids/ExternalIdUpsertPreprocessor.java
index c0697db..6d21072 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdUpsertPreprocessor.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdUpsertPreprocessor.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.account.externalids;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
/**
* This optional preprocessor is called in {@link ExternalIdNotes} before an update is committed.
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java b/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
index 4e67e3d..56115b2 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
@@ -14,140 +14,17 @@
package com.google.gerrit.server.account.externalids;
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
-import static java.util.stream.Collectors.joining;
-
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.HashedPassword;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.Note;
-import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.RevWalk;
-@Singleton
-public class ExternalIdsConsistencyChecker {
- private final GitRepositoryManager repoManager;
- private final AllUsersName allUsers;
- private final AccountCache accountCache;
- private final OutgoingEmailValidator validator;
- private final ExternalIdFactory externalIdFactory;
+public interface ExternalIdsConsistencyChecker {
+ List<ConsistencyCheckInfo.ConsistencyProblemInfo> check(AccountCache accountCache)
+ throws IOException, ConfigInvalidException;
- @Inject
- ExternalIdsConsistencyChecker(
- GitRepositoryManager repoManager,
- AllUsersName allUsers,
- AccountCache accountCache,
- OutgoingEmailValidator validator,
- ExternalIdFactory externalIdFactory) {
- this.repoManager = repoManager;
- this.allUsers = allUsers;
- this.accountCache = accountCache;
- this.validator = validator;
- this.externalIdFactory = externalIdFactory;
- }
-
- public List<ConsistencyProblemInfo> check() throws IOException, ConfigInvalidException {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- return check(ExternalIdNotes.loadReadOnly(allUsers, repo, null, externalIdFactory, false));
- }
- }
-
- public List<ConsistencyProblemInfo> check(ObjectId rev)
- throws IOException, ConfigInvalidException {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- return check(ExternalIdNotes.loadReadOnly(allUsers, repo, rev, externalIdFactory, false));
- }
- }
-
- private List<ConsistencyProblemInfo> check(ExternalIdNotes extIdNotes) throws IOException {
- List<ConsistencyProblemInfo> problems = new ArrayList<>();
-
- ListMultimap<String, ExternalId> emails = MultimapBuilder.hashKeys().arrayListValues().build();
-
- try (RevWalk rw = new RevWalk(extIdNotes.getRepository())) {
- NoteMap noteMap = extIdNotes.getNoteMap();
- for (Note note : noteMap) {
- byte[] raw = ExternalIdNotes.readNoteData(rw, note.getData());
- try {
- ExternalId extId = externalIdFactory.parse(note.getName(), raw, note.getData());
- problems.addAll(validateExternalId(extId));
-
- if (extId.email() != null) {
- String email = extId.email();
- if (emails.get(email).stream()
- .noneMatch(e -> e.accountId().get() == extId.accountId().get())) {
- emails.put(email, extId);
- }
- }
- } catch (ConfigInvalidException e) {
- addError(String.format(e.getMessage()), problems);
- }
- }
- }
-
- emails.asMap().entrySet().stream()
- .filter(e -> e.getValue().size() > 1)
- .forEach(
- e ->
- addError(
- String.format(
- "Email '%s' is not unique, it's used by the following external IDs: %s",
- e.getKey(),
- e.getValue().stream()
- .map(k -> "'" + k.key().get() + "'")
- .sorted()
- .collect(joining(", "))),
- problems));
-
- return problems;
- }
-
- private List<ConsistencyProblemInfo> validateExternalId(ExternalId extId) {
- List<ConsistencyProblemInfo> problems = new ArrayList<>();
-
- if (!accountCache.get(extId.accountId()).isPresent()) {
- addError(
- String.format(
- "External ID '%s' belongs to account that doesn't exist: %s",
- extId.key().get(), extId.accountId().get()),
- problems);
- }
-
- if (extId.email() != null && !validator.isValid(extId.email())) {
- addError(
- String.format(
- "External ID '%s' has an invalid email: %s", extId.key().get(), extId.email()),
- problems);
- }
-
- if (extId.password() != null && extId.isScheme(SCHEME_USERNAME)) {
- try {
- HashedPassword.decode(extId.password());
- } catch (HashedPassword.DecoderException e) {
- addError(
- String.format(
- "External ID '%s' has an invalid password: %s", extId.key().get(), e.getMessage()),
- problems);
- }
- }
-
- return problems;
- }
-
- private static void addError(String error, List<ConsistencyProblemInfo> problems) {
- problems.add(new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, error));
- }
+ List<ConsistencyCheckInfo.ConsistencyProblemInfo> check(AccountCache accountCache, ObjectId rev)
+ throws IOException, ConfigInvalidException;
}
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIds.java
similarity index 96%
rename from java/com/google/gerrit/server/account/externalids/AllExternalIds.java
rename to java/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIds.java
index 14aa368..8660b01 100644
--- a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIds.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account.externalids;
+package com.google.gerrit.server.account.externalids.storage.notedb;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
@@ -20,6 +20,7 @@
import com.google.common.collect.ImmutableSetMultimap;
import com.google.gerrit.entities.Account;
import com.google.gerrit.proto.Protos;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto;
import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto.ExternalIdProto;
import com.google.gerrit.server.cache.serialize.CacheSerializer;
diff --git a/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/DisabledExternalIdCache.java
similarity index 88%
rename from java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
rename to java/com/google/gerrit/server/account/externalids/storage/notedb/DisabledExternalIdCache.java
index 2d1ec1a..8e53277 100644
--- a/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/DisabledExternalIdCache.java
@@ -12,16 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account.externalids;
+package com.google.gerrit.server.account.externalids.storage.notedb;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdCache;
import com.google.inject.AbstractModule;
import com.google.inject.Module;
import java.io.IOException;
import java.util.Optional;
-import org.eclipse.jgit.lib.ObjectId;
public class DisabledExternalIdCache implements ExternalIdCache {
public static Module module() {
@@ -45,11 +46,6 @@
}
@Override
- public ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) {
- throw new UnsupportedOperationException();
- }
-
- @Override
public ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException {
throw new UnsupportedOperationException();
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheImpl.java
similarity index 93%
rename from java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
rename to java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheImpl.java
index 1f715c8a..d7ff85c 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheImpl.java
@@ -12,13 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account.externalids;
+package com.google.gerrit.server.account.externalids.storage.notedb;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdReader;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdCache;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
@@ -60,8 +61,7 @@
return get().byAccount().get(accountId);
}
- @Override
- public ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException {
+ ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException {
return get(rev).byAccount().get(accountId);
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java
similarity index 97%
rename from java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
rename to java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java
index 9ba2558..db890fc 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account.externalids;
+package com.google.gerrit.server.account.externalids.storage.notedb;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
@@ -29,7 +29,7 @@
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0;
-import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdReader;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -74,7 +74,7 @@
private final Counter1<Boolean> reloadCounter;
private final Timer0 reloadDifferential;
private final boolean isPersistentCache;
- private final ExternalIdFactory externalIdFactory;
+ private final ExternalIdFactoryNoteDbImpl externalIdFactory;
@Inject
ExternalIdCacheLoader(
@@ -84,7 +84,7 @@
@Named(ExternalIdCacheImpl.CACHE_NAME) Cache<ObjectId, AllExternalIds> externalIdCache,
MetricMaker metricMaker,
@GerritServerConfig Config config,
- ExternalIdFactory externalIdFactory) {
+ ExternalIdFactoryNoteDbImpl externalIdFactory) {
this.externalIdReader = externalIdReader;
this.externalIdCache = externalIdCache;
this.gitRepositoryManager = gitRepositoryManager;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheModule.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheModule.java
similarity index 93%
rename from java/com/google/gerrit/server/account/externalids/ExternalIdCacheModule.java
rename to java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheModule.java
index 1873ea0..aca0e1a 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheModule.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheModule.java
@@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account.externalids;
+package com.google.gerrit.server.account.externalids.storage.notedb;
+import com.google.gerrit.server.account.externalids.ExternalIdCache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
import com.google.inject.TypeLiteral;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCaseSensitivityMigrator.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCaseSensitivityMigrator.java
index 2e583ee..4e1323e 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCaseSensitivityMigrator.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCaseSensitivityMigrator.java
@@ -21,9 +21,7 @@
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -61,7 +59,7 @@
private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
private final ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
- private final ExternalIdFactory externalIdFactory;
+ private final ExternalIdFactoryNoteDbImpl externalIdFactory;
private final Boolean isUserNameCaseInsensitive;
private final Boolean dryRun;
@@ -71,7 +69,7 @@
AllUsersName allUsersName,
Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
ExternalIdNotes.FactoryNoReindex externalIdNotesFactory,
- ExternalIdFactory externalIdFactory,
+ ExternalIdFactoryNoteDbImpl externalIdFactory,
@Assisted("isUserNameCaseInsensitive") Boolean isUserNameCaseInsensitive,
@Assisted("dryRun") Boolean dryRun) {
this.repoManager = repoManager;
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
new file mode 100644
index 0000000..3462c76
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
@@ -0,0 +1,273 @@
+// Copyright (C) 2023 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.account.externalids.storage.notedb;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.HashedPassword;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.config.AuthConfig;
+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;
+
+@Singleton
+public class ExternalIdFactoryNoteDbImpl implements ExternalIdFactory {
+ private final ExternalIdKeyFactory externalIdKeyFactory;
+ private AuthConfig authConfig;
+
+ @Inject
+ @VisibleForTesting
+ public ExternalIdFactoryNoteDbImpl(
+ ExternalIdKeyFactory externalIdKeyFactory, AuthConfig authConfig) {
+ this.externalIdKeyFactory = externalIdKeyFactory;
+ this.authConfig = authConfig;
+ }
+
+ @Override
+ public ExternalId create(String scheme, String id, Account.Id accountId) {
+ return create(externalIdKeyFactory.create(scheme, id), accountId, null, null);
+ }
+
+ @Override
+ public ExternalId create(
+ String scheme,
+ String id,
+ Account.Id accountId,
+ @Nullable String email,
+ @Nullable String hashedPassword) {
+ return create(externalIdKeyFactory.create(scheme, id), accountId, email, hashedPassword);
+ }
+
+ @Override
+ public ExternalId create(ExternalId.Key key, Account.Id accountId) {
+ return create(key, accountId, null, null);
+ }
+
+ @Override
+ public ExternalId create(
+ ExternalId.Key key,
+ Account.Id accountId,
+ @Nullable String email,
+ @Nullable String hashedPassword) {
+ return create(
+ key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
+ }
+
+ ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
+ return create(extId.key(), extId.accountId(), extId.email(), extId.password(), blobId);
+ }
+
+ /**
+ * Creates an external ID.
+ *
+ * @param key the external Id key
+ * @param accountId the ID of the account to which the external ID belongs
+ * @param email the email of the external ID, may be {@code null}
+ * @param hashedPassword the hashed password of the external ID, may be {@code null}
+ * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
+ * {@code null} if the external ID was created in code and is not yet stored in Git.
+ * @return the created external ID
+ */
+ public ExternalId create(
+ ExternalId.Key key,
+ Account.Id accountId,
+ @Nullable String email,
+ @Nullable String hashedPassword,
+ @Nullable ObjectId blobId) {
+ return ExternalId.create(
+ key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
+ }
+
+ @Override
+ public ExternalId createWithPassword(
+ ExternalId.Key key,
+ Account.Id accountId,
+ @Nullable String email,
+ @Nullable String plainPassword) {
+ plainPassword = Strings.emptyToNull(plainPassword);
+ String hashedPassword =
+ plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
+ return create(key, accountId, email, hashedPassword);
+ }
+
+ @Override
+ public ExternalId createUsername(
+ String id, Account.Id accountId, @Nullable String plainPassword) {
+ return createWithPassword(
+ externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, id),
+ accountId,
+ null,
+ plainPassword);
+ }
+
+ @Override
+ public ExternalId createWithEmail(
+ String scheme, String id, Account.Id accountId, @Nullable String email) {
+ return createWithEmail(externalIdKeyFactory.create(scheme, id), accountId, email);
+ }
+
+ @Override
+ public ExternalId createWithEmail(
+ ExternalId.Key key, Account.Id accountId, @Nullable String email) {
+ return create(key, accountId, Strings.emptyToNull(email), null);
+ }
+
+ @Override
+ public ExternalId createEmail(Account.Id accountId, String email) {
+ return createWithEmail(ExternalId.SCHEME_MAILTO, email, accountId, requireNonNull(email));
+ }
+
+ /**
+ * Parses an external ID from a byte array that contains the external ID as a Git config file
+ * text.
+ *
+ * <p>The Git config must have exactly one externalId subsection with an accountId and optionally
+ * email and password:
+ *
+ * <pre>
+ * [externalId "username:jdoe"]
+ * accountId = 1003407
+ * email = jdoe@example.com
+ * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
+ * </pre>
+ *
+ * @param noteId the SHA-1 sum of the external ID used as the note's ID
+ * @param raw a byte array that contains the external ID as a Git config file text.
+ * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
+ * {@code null} if the external ID was created in code and is not yet stored in Git.
+ * @return the parsed external ID
+ */
+ public ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
+ throws ConfigInvalidException {
+ requireNonNull(blobId);
+
+ Config externalIdConfig = new Config();
+ try {
+ externalIdConfig.fromText(new String(raw, UTF_8));
+ } catch (ConfigInvalidException e) {
+ throw invalidConfig(noteId, e.getMessage());
+ }
+
+ Set<String> externalIdKeys = externalIdConfig.getSubsections(ExternalId.EXTERNAL_ID_SECTION);
+ if (externalIdKeys.size() != 1) {
+ throw invalidConfig(
+ noteId,
+ String.format(
+ "Expected exactly 1 '%s' section, found %d",
+ ExternalId.EXTERNAL_ID_SECTION, externalIdKeys.size()));
+ }
+
+ String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
+ ExternalId.Key externalIdKey = externalIdKeyFactory.parse(externalIdKeyStr);
+ if (externalIdKey == null) {
+ throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
+ }
+
+ if (!externalIdKey.sha1().getName().equals(noteId)) {
+ if (!authConfig.isUserNameCaseInsensitiveMigrationMode()) {
+ throw invalidConfig(
+ noteId,
+ String.format(
+ "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
+ }
+
+ if (!externalIdKey.caseSensitiveSha1().getName().equals(noteId)) {
+ throw invalidConfig(
+ noteId,
+ String.format(
+ "Neither case sensitive nor case insensitive SHA1 of external ID '%s' match note ID"
+ + " '%s'",
+ externalIdKeyStr, noteId));
+ }
+ externalIdKey =
+ externalIdKeyFactory.create(externalIdKey.scheme(), externalIdKey.id(), false);
+ }
+
+ String email =
+ externalIdConfig.getString(
+ ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.EMAIL_KEY);
+ String password =
+ externalIdConfig.getString(
+ ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.PASSWORD_KEY);
+ int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
+
+ return create(
+ externalIdKey,
+ Account.id(accountId),
+ Strings.emptyToNull(email),
+ Strings.emptyToNull(password),
+ blobId);
+ }
+
+ private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
+ throws ConfigInvalidException {
+ String accountIdStr =
+ externalIdConfig.getString(
+ ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY);
+ if (accountIdStr == null) {
+ throw invalidConfig(
+ noteId,
+ String.format(
+ "Value for '%s.%s.%s' is missing, expected account ID",
+ ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY));
+ }
+
+ try {
+ int accountId =
+ externalIdConfig.getInt(
+ ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY, -1);
+ if (accountId < 0) {
+ throw invalidConfig(
+ noteId,
+ String.format(
+ "Value %s for '%s.%s.%s' is invalid, expected account ID",
+ accountIdStr,
+ ExternalId.EXTERNAL_ID_SECTION,
+ externalIdKeyStr,
+ ExternalId.ACCOUNT_ID_KEY));
+ }
+ return accountId;
+ } catch (IllegalArgumentException e) {
+ ConfigInvalidException newException =
+ invalidConfig(
+ noteId,
+ String.format(
+ "Value %s for '%s.%s.%s' is invalid, expected account ID",
+ accountIdStr,
+ ExternalId.EXTERNAL_ID_SECTION,
+ externalIdKeyStr,
+ ExternalId.ACCOUNT_ID_KEY));
+ newException.initCause(e);
+ throw newException;
+ }
+ }
+
+ private static ConfigInvalidException invalidConfig(String noteId, String message) {
+ return new ConfigInvalidException(
+ String.format("Invalid external ID config for note '%s': %s", noteId, message));
+ }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNoteDbStorageModule.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNoteDbStorageModule.java
index e76740c..99ac568 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNoteDbStorageModule.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNoteDbStorageModule.java
@@ -15,8 +15,10 @@
package com.google.gerrit.server.account.externalids.storage.notedb;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
@@ -26,5 +28,9 @@
DynamicMap.mapOf(binder(), ExternalIdUpsertPreprocessor.class);
bind(ExternalIds.class).to(ExternalIdsNoteDbImpl.class).in(Singleton.class);
+ bind(ExternalIdsConsistencyChecker.class)
+ .to(ExternalIdsConsistencyCheckerNoteDbImpl.class)
+ .in(Singleton.class);
+ bind(ExternalIdFactory.class).to(ExternalIdFactoryNoteDbImpl.class).in(Singleton.class);
}
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
similarity index 97%
rename from java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
rename to java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
index 48c403c..5346252 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account.externalids;
+package com.google.gerrit.server.account.externalids.storage.notedb;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -26,6 +26,7 @@
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
@@ -36,6 +37,11 @@
import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdCache;
+import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -101,7 +107,7 @@
protected final MetricMaker metricMaker;
protected final AllUsersName allUsersName;
protected final DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors;
- protected final ExternalIdFactory externalIdFactory;
+ protected final ExternalIdFactoryNoteDbImpl externalIdFactory;
protected final AuthConfig authConfig;
protected ExternalIdNotesLoader(
@@ -109,7 +115,7 @@
MetricMaker metricMaker,
AllUsersName allUsersName,
DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
- ExternalIdFactory externalIdFactory,
+ ExternalIdFactoryNoteDbImpl externalIdFactory,
AuthConfig authConfig) {
this.externalIdCache = externalIdCache;
this.metricMaker = metricMaker;
@@ -197,7 +203,7 @@
MetricMaker metricMaker,
AllUsersName allUsersName,
DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
- ExternalIdFactory externalIdFactory,
+ ExternalIdFactoryNoteDbImpl externalIdFactory,
AuthConfig authConfig) {
super(
externalIdCache,
@@ -250,7 +256,7 @@
MetricMaker metricMaker,
AllUsersName allUsersName,
DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
- ExternalIdFactory externalIdFactory,
+ ExternalIdFactoryNoteDbImpl externalIdFactory,
AuthConfig authConfig) {
super(
externalIdCache,
@@ -309,7 +315,7 @@
AllUsersName allUsersName,
Repository allUsersRepo,
@Nullable ObjectId rev,
- ExternalIdFactory externalIdFactory,
+ ExternalIdFactoryNoteDbImpl externalIdFactory,
boolean isUserNameCaseInsensitiveMigrationMode)
throws IOException, ConfigInvalidException {
return new ExternalIdNotes(
@@ -337,7 +343,7 @@
public static ExternalIdNotes load(
AllUsersName allUsersName,
Repository allUsersRepo,
- ExternalIdFactory externalIdFactory,
+ ExternalIdFactoryNoteDbImpl externalIdFactory,
boolean isUserNameCaseInsensitiveMigrationMode)
throws IOException, ConfigInvalidException {
return new ExternalIdNotes(
@@ -356,7 +362,7 @@
private final Repository repo;
private final DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors;
private final CallerFinder callerFinder;
- private final ExternalIdFactory externalIdFactory;
+ private final ExternalIdFactoryNoteDbImpl externalIdFactory;
private NoteMap noteMap;
private ObjectId oldRev;
@@ -400,7 +406,7 @@
AllUsersName allUsersName,
Repository allUsersRepo,
DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
- ExternalIdFactory externalIdFactory,
+ ExternalIdFactoryNoteDbImpl externalIdFactory,
boolean isUserNameCaseInsensitiveMigrationMode) {
this.updateCount =
metricMaker.newCounter(
@@ -859,6 +865,7 @@
}
@Override
+ @CanIgnoreReturnValue
public RevCommit commit(MetaDataUpdate update) throws IOException {
oldRev = ObjectIds.copyOrZero(revision);
RevCommit commit = super.commit(update);
@@ -916,6 +923,7 @@
*
* @return the ID of the account to which all specified external IDs belong.
*/
+ @CanIgnoreReturnValue
public static Account.Id checkSameAccount(
Iterable<ExternalId> extIds, @Nullable Account.Id accountId) {
for (ExternalId extId : extIds) {
@@ -1091,6 +1099,7 @@
final Set<ExternalId> added = new HashSet<>();
final Set<ExternalId> removed = new HashSet<>();
+ @CanIgnoreReturnValue
ExternalIdCacheUpdates add(Collection<ExternalId> extIds) {
this.added.addAll(extIds);
return this;
@@ -1100,6 +1109,7 @@
return ImmutableSet.copyOf(added);
}
+ @CanIgnoreReturnValue
ExternalIdCacheUpdates remove(Collection<ExternalId> extIds) {
this.removed.addAll(extIds);
return this;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
index 4bab7e1..dbaed04 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
@@ -23,8 +23,6 @@
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -73,7 +71,7 @@
private boolean failOnLoad = false;
private final Timer0 readAllLatency;
private final Timer0 readSingleLatency;
- private final ExternalIdFactory externalIdFactory;
+ private final ExternalIdFactoryNoteDbImpl externalIdFactory;
private final AuthConfig authConfig;
@VisibleForTesting
@@ -82,7 +80,7 @@
GitRepositoryManager repoManager,
AllUsersName allUsersName,
MetricMaker metricMaker,
- ExternalIdFactory externalIdFactory,
+ ExternalIdFactoryNoteDbImpl externalIdFactory,
AuthConfig authConfig) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
new file mode 100644
index 0000000..83c72f1
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
@@ -0,0 +1,166 @@
+// Copyright (C) 2023 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.account.externalids.storage.notedb;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.HashedPassword;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+@Singleton
+public class ExternalIdsConsistencyCheckerNoteDbImpl implements ExternalIdsConsistencyChecker {
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsers;
+ private final OutgoingEmailValidator validator;
+ private final ExternalIdFactoryNoteDbImpl externalIdFactory;
+
+ @Inject
+ ExternalIdsConsistencyCheckerNoteDbImpl(
+ GitRepositoryManager repoManager,
+ AllUsersName allUsers,
+ OutgoingEmailValidator validator,
+ ExternalIdFactory externalIdFactory) {
+ this.repoManager = repoManager;
+ this.allUsers = allUsers;
+ this.validator = validator;
+ checkState(
+ externalIdFactory instanceof ExternalIdFactoryNoteDbImpl,
+ "ExternalIdsConsistencyCheckerNoteDbImpl must be initiated with ExternalIdFactoryNoteDbImpl.");
+ this.externalIdFactory = (ExternalIdFactoryNoteDbImpl) externalIdFactory;
+ }
+
+ @Override
+ public List<ConsistencyProblemInfo> check(AccountCache accountCache)
+ throws IOException, ConfigInvalidException {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return check(
+ accountCache,
+ ExternalIdNotes.loadReadOnly(allUsers, repo, null, externalIdFactory, false));
+ }
+ }
+
+ @Override
+ public List<ConsistencyProblemInfo> check(AccountCache accountCache, ObjectId rev)
+ throws IOException, ConfigInvalidException {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return check(
+ accountCache,
+ ExternalIdNotes.loadReadOnly(allUsers, repo, rev, externalIdFactory, false));
+ }
+ }
+
+ private List<ConsistencyProblemInfo> check(AccountCache accountCache, ExternalIdNotes extIdNotes)
+ throws IOException {
+ List<ConsistencyProblemInfo> problems = new ArrayList<>();
+
+ ListMultimap<String, ExternalId> emails = MultimapBuilder.hashKeys().arrayListValues().build();
+
+ try (RevWalk rw = new RevWalk(extIdNotes.getRepository())) {
+ NoteMap noteMap = extIdNotes.getNoteMap();
+ for (Note note : noteMap) {
+ byte[] raw = ExternalIdNotes.readNoteData(rw, note.getData());
+ try {
+ ExternalId extId = externalIdFactory.parse(note.getName(), raw, note.getData());
+ problems.addAll(validateExternalId(accountCache, extId));
+
+ if (extId.email() != null) {
+ String email = extId.email();
+ if (emails.get(email).stream()
+ .noneMatch(e -> e.accountId().get() == extId.accountId().get())) {
+ emails.put(email, extId);
+ }
+ }
+ } catch (ConfigInvalidException e) {
+ addError(String.format(e.getMessage()), problems);
+ }
+ }
+ }
+
+ emails.asMap().entrySet().stream()
+ .filter(e -> e.getValue().size() > 1)
+ .forEach(
+ e ->
+ addError(
+ String.format(
+ "Email '%s' is not unique, it's used by the following external IDs: %s",
+ e.getKey(),
+ e.getValue().stream()
+ .map(k -> "'" + k.key().get() + "'")
+ .sorted()
+ .collect(joining(", "))),
+ problems));
+
+ return problems;
+ }
+
+ private List<ConsistencyProblemInfo> validateExternalId(
+ AccountCache accountCache, ExternalId extId) {
+ List<ConsistencyProblemInfo> problems = new ArrayList<>();
+
+ if (accountCache.get(extId.accountId()).isEmpty()) {
+ addError(
+ String.format(
+ "External ID '%s' belongs to account that doesn't exist: %s",
+ extId.key().get(), extId.accountId().get()),
+ problems);
+ }
+
+ if (extId.email() != null && !validator.isValid(extId.email())) {
+ addError(
+ String.format(
+ "External ID '%s' has an invalid email: %s", extId.key().get(), extId.email()),
+ problems);
+ }
+
+ if (extId.password() != null && extId.isScheme(SCHEME_USERNAME)) {
+ try {
+ HashedPassword.decode(extId.password());
+ } catch (HashedPassword.DecoderException e) {
+ addError(
+ String.format(
+ "External ID '%s' has an invalid password: %s", extId.key().get(), e.getMessage()),
+ problems);
+ }
+ }
+
+ return problems;
+ }
+
+ private static void addError(String error, List<ConsistencyProblemInfo> problems) {
+ problems.add(new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, error));
+ }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java
index 6effb7e..b09099d 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdCache;
@@ -39,7 +40,7 @@
@Singleton
public class ExternalIdsNoteDbImpl implements ExternalIds {
private final ExternalIdReader externalIdReader;
- private final ExternalIdCache externalIdCache;
+ @Nullable private final ExternalIdCacheImpl externalIdCache;
private final AuthConfig authConfig;
private final ExternalIdKeyFactory externalIdKeyFactory;
@@ -50,7 +51,16 @@
ExternalIdKeyFactory externalIdKeyFactory,
AuthConfig authConfig) {
this.externalIdReader = externalIdReader;
- this.externalIdCache = externalIdCache;
+ if (externalIdCache instanceof ExternalIdCacheImpl) {
+ this.externalIdCache = (ExternalIdCacheImpl) externalIdCache;
+ } else if (externalIdCache instanceof DisabledExternalIdCache) {
+ // Supported case for testing only. Non of the disabled cache methods should be called, so
+ // it's safe to not assign the var.
+ this.externalIdCache = null;
+ } else {
+ throw new IllegalStateException(
+ "The cache provided in ExternalIdsNoteDbImpl should be either ExternalIdCacheImpl or DisabledExternalIdCache");
+ }
this.externalIdKeyFactory = externalIdKeyFactory;
this.authConfig = authConfig;
}
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 314f122..f0343b5 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountsNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountsNoteDbImpl.java
@@ -32,7 +32,7 @@
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.ProjectWatches;
import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdsNoteDbImpl;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.CachedPreferences;
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 9eb0f49..d6b87a3 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
@@ -43,7 +43,7 @@
import com.google.gerrit.server.account.ProjectWatches;
import com.google.gerrit.server.account.StoredPreferences;
import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdsNoteDbImpl;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.CachedPreferences;
diff --git a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index f311b35..78cf811 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -24,7 +24,6 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.restapi.project.ListProjects;
import com.google.gerrit.server.restapi.project.ListProjects.FilterType;
import com.google.gerrit.server.restapi.project.ProjectsCollection;
@@ -94,8 +93,7 @@
};
}
- private SortedMap<String, ProjectInfo> list(ListRequest request)
- throws RestApiException, PermissionBackendException {
+ private SortedMap<String, ProjectInfo> list(ListRequest request) throws Exception {
ListProjects lp = listProvider.get();
lp.setShowDescription(request.getDescription());
lp.setLimit(request.getLimit());
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index dc8a7dc..52230ba 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -145,7 +145,6 @@
copy.revertOf = changeInfo.revertOf;
copy.submissionId = changeInfo.submissionId;
copy.starred = changeInfo.starred;
- copy.stars = changeInfo.stars;
copy.submitted = changeInfo.submitted;
copy.submitter = changeInfo.submitter;
copy.unresolvedCommentCount = changeInfo.unresolvedCommentCount;
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 2818f87..716295f 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -46,7 +46,6 @@
import com.google.gerrit.entities.SubmissionId;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -59,13 +58,13 @@
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModificationList;
import com.google.gerrit.server.config.SendEmailExecutor;
-import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.mail.EmailFactories;
import com.google.gerrit.server.mail.send.ChangeEmail;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
@@ -119,12 +118,13 @@
private final EmailFactories emailFactories;
private final ExecutorService sendEmailExecutor;
private final CommitValidators.Factory commitValidatorsFactory;
+ private final TopicValidator topicValidator;
private final RevisionCreated revisionCreated;
private final CommentAdded commentAdded;
private final ReviewerModifier reviewerModifier;
private final MessageIdGenerator messageIdGenerator;
- private final DynamicItem<UrlFormatter> urlFormatter;
private final AutoMerger autoMerger;
+ private final ChangeUtil changeUtil;
private final Change.Id changeId;
private final PatchSet.Id psId;
@@ -172,12 +172,13 @@
EmailFactories emailFactories,
@SendEmailExecutor ExecutorService sendEmailExecutor,
CommitValidators.Factory commitValidatorsFactory,
+ TopicValidator topicValidator,
CommentAdded commentAdded,
RevisionCreated revisionCreated,
ReviewerModifier reviewerModifier,
MessageIdGenerator messageIdGenerator,
- DynamicItem<UrlFormatter> urlFormatter,
AutoMerger autoMerger,
+ ChangeUtil changeUtil,
@Assisted Change.Id changeId,
@Assisted ObjectId commitId,
@Assisted String refName) {
@@ -190,12 +191,13 @@
this.emailFactories = emailFactories;
this.sendEmailExecutor = sendEmailExecutor;
this.commitValidatorsFactory = commitValidatorsFactory;
+ this.topicValidator = topicValidator;
this.revisionCreated = revisionCreated;
this.commentAdded = commentAdded;
this.reviewerModifier = reviewerModifier;
this.messageIdGenerator = messageIdGenerator;
- this.urlFormatter = urlFormatter;
this.autoMerger = autoMerger;
+ this.changeUtil = changeUtil;
this.changeId = changeId;
this.psId = PatchSet.id(changeId, INITIAL_PATCH_SET_ID);
@@ -230,7 +232,7 @@
private Change.Key getChangeKey(RevWalk rw) throws IOException {
RevCommit commit = rw.parseCommit(commitId);
rw.parseBody(commit);
- List<String> idList = ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter.get());
+ List<String> idList = changeUtil.getChangeIdsFromFooter(commit);
if (!idList.isEmpty()) {
return Change.key(idList.get(idList.size() - 1).trim());
}
@@ -471,7 +473,7 @@
update.setSubjectForCommit("Create change");
update.setBranch(change.getDest().branch());
try {
- update.setTopic(change.getTopic());
+ update.setTopic(change.getTopic(), topicValidator);
} catch (ValidationException ex) {
throw new BadRequestException(ex.getMessage());
}
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 8d42fcc..e41566c 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -693,10 +693,8 @@
}
if (user.isIdentifiedUser()) {
- Collection<String> stars = cd.stars(user.getAccountId());
- out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null;
- if (!stars.isEmpty()) {
- out.stars = stars;
+ if (cd.isStarred(user.getAccountId())) {
+ out.starred = true;
}
}
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index c5c0be0..234cd2e 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -237,7 +237,7 @@
.build())) {
Hasher h = Hashing.murmur3_128().newHasher();
if (user.isIdentifiedUser()) {
- h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
+ h.putBoolean(starredChangesUtil.isStarred(user.getAccountId(), getId()));
}
prepareETag(h, user);
return h.hash().toString();
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 063903b..b216db3 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -38,14 +38,12 @@
import com.google.gerrit.extensions.api.changes.FixInput;
import com.google.gerrit.extensions.common.ProblemInfo;
import com.google.gerrit.extensions.common.ProblemInfo.Status;
-import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.Accounts;
-import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.PatchSetState;
@@ -118,7 +116,7 @@
private final Provider<CurrentUser> user;
private final Provider<PersonIdent> serverIdent;
private final RetryHelper retryHelper;
- private final DynamicItem<UrlFormatter> urlFormatter;
+ private final ChangeUtil changeUtil;
private BatchUpdate.Factory updateFactory;
private FixInput fix;
@@ -146,7 +144,7 @@
PatchSetUtil psUtil,
Provider<CurrentUser> user,
RetryHelper retryHelper,
- DynamicItem<UrlFormatter> urlFormatter) {
+ ChangeUtil changeUtil) {
this.accounts = accounts;
this.accountPatchReviewStore = accountPatchReviewStore;
this.notesFactory = notesFactory;
@@ -157,7 +155,7 @@
this.retryHelper = retryHelper;
this.serverIdent = serverIdent;
this.user = user;
- this.urlFormatter = urlFormatter;
+ this.changeUtil = changeUtil;
reset();
}
@@ -463,9 +461,7 @@
case 0:
// No patch set for this commit; insert one.
rw.parseBody(commit);
- String changeId =
- Iterables.getFirst(
- ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter.get()), null);
+ String changeId = Iterables.getFirst(changeUtil.getChangeIdsFromFooter(commit), null);
// Missing Change-Id footer is ok, but mismatched is not.
if (changeId != null && !changeId.equals(change().getKey().get())) {
problem(
diff --git a/java/com/google/gerrit/server/change/CustomKeyedValuesUtil.java b/java/com/google/gerrit/server/change/CustomKeyedValuesUtil.java
index 04bc6e4..55b4d74 100644
--- a/java/com/google/gerrit/server/change/CustomKeyedValuesUtil.java
+++ b/java/com/google/gerrit/server/change/CustomKeyedValuesUtil.java
@@ -17,6 +17,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.Map;
+import java.util.Set;
public class CustomKeyedValuesUtil {
public static class InvalidCustomKeyedValueException extends Exception {
@@ -35,7 +36,7 @@
}
}
- static ImmutableMap<String, String> extractCustomKeyedValues(ImmutableMap<String, String> input)
+ static ImmutableMap<String, String> extractCustomKeyedValues(Map<String, String> input)
throws InvalidCustomKeyedValueException {
if (input == null) {
return ImmutableMap.of();
@@ -57,7 +58,7 @@
return builder.build();
}
- static ImmutableSet<String> extractCustomKeys(ImmutableSet<String> input)
+ static ImmutableSet<String> extractCustomKeys(Set<String> input)
throws InvalidCustomKeyedValueException {
if (input == null) {
return ImmutableSet.of();
diff --git a/java/com/google/gerrit/server/change/FileContentUtil.java b/java/com/google/gerrit/server/change/FileContentUtil.java
index c54b902..4c0eb69 100644
--- a/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -25,6 +25,7 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.project.ProjectState;
@@ -39,6 +40,7 @@
import java.util.zip.ZipOutputStream;
import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
@@ -60,11 +62,15 @@
private final GitRepositoryManager repoManager;
private final FileTypeRegistry registry;
+ private final long maxFileSizeBytes;
@Inject
- FileContentUtil(GitRepositoryManager repoManager, FileTypeRegistry ftr) {
+ FileContentUtil(
+ GitRepositoryManager repoManager, FileTypeRegistry ftr, @GerritServerConfig Config config) {
this.repoManager = repoManager;
this.registry = ftr;
+ long maxFileSizeDownload = config.getLong("change", null, "maxFileSizeDownload", 0);
+ this.maxFileSizeBytes = maxFileSizeDownload > 0 ? maxFileSizeDownload : Long.MAX_VALUE;
}
/**
@@ -117,6 +123,7 @@
}
ObjectLoader obj = repo.open(id, OBJ_BLOB);
+ checkMaxFileSizeBytes(obj);
byte[] raw;
try {
raw = obj.getCachedBytes(MAX_SIZE);
@@ -154,7 +161,7 @@
public BinaryResult downloadContent(
ProjectState project, ObjectId revstr, String path, @Nullable Integer parent)
- throws ResourceNotFoundException, IOException {
+ throws ResourceNotFoundException, IOException, BadRequestException {
try (Repository repo = openRepository(project);
RevWalk rw = new RevWalk(repo)) {
String suffix = "new";
@@ -179,6 +186,8 @@
ObjectId id = tw.getObjectId(0);
ObjectLoader obj = repo.open(id, OBJ_BLOB);
+ checkMaxFileSizeBytes(obj);
+
byte[] raw;
try {
raw = obj.getCachedBytes(MAX_SIZE);
@@ -194,6 +203,16 @@
}
}
+ private void checkMaxFileSizeBytes(ObjectLoader obj) throws BadRequestException {
+ if (obj.getSize() > this.maxFileSizeBytes) {
+ throw new BadRequestException(
+ String.format(
+ "File too big. File size: %d bytes. Configured 'maxFileSizeDownload' limit: %d"
+ + " bytes.",
+ obj.getSize(), this.maxFileSizeBytes));
+ }
+ }
+
private BinaryResult wrapBlob(
String path,
final ObjectLoader obj,
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 4a09f84..854fd4e 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -43,6 +43,7 @@
import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.AutoMerger;
@@ -84,6 +85,7 @@
private final PatchSetUtil psUtil;
private final WorkInProgressStateChanged wipStateChanged;
private final AutoMerger autoMerger;
+ private final TopicValidator topicValidator;
// Assisted-injected fields.
private final PatchSet.Id psId;
@@ -132,6 +134,7 @@
ProjectCache projectCache,
WorkInProgressStateChanged wipStateChanged,
AutoMerger autoMerger,
+ TopicValidator topicValidator,
@Assisted ChangeNotes notes,
@Assisted PatchSet.Id psId,
@Assisted ObjectId commitId) {
@@ -147,6 +150,7 @@
this.projectCache = projectCache;
this.wipStateChanged = wipStateChanged;
this.autoMerger = autoMerger;
+ this.topicValidator = topicValidator;
this.origNotes = notes;
this.psId = psId;
@@ -307,7 +311,7 @@
if (topic != null) {
change.setTopic(topic);
try {
- update.setTopic(topic);
+ update.setTopic(topic, topicValidator);
} catch (ValidationException ex) {
throw new BadRequestException(ex.getMessage());
}
diff --git a/java/com/google/gerrit/server/change/SetTopicOp.java b/java/com/google/gerrit/server/change/SetTopicOp.java
index ee35d1d..58342e5 100644
--- a/java/com/google/gerrit/server/change/SetTopicOp.java
+++ b/java/com/google/gerrit/server/change/SetTopicOp.java
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.extensions.events.TopicEdited;
+import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -36,6 +37,7 @@
private final String topic;
private final TopicEdited topicEdited;
private final ChangeMessagesUtil cmUtil;
+ private final TopicValidator topicValidator;
private Change change;
private String oldTopicName;
@@ -43,10 +45,14 @@
@Inject
public SetTopicOp(
- TopicEdited topicEdited, ChangeMessagesUtil cmUtil, @Nullable @Assisted String topic) {
+ TopicEdited topicEdited,
+ ChangeMessagesUtil cmUtil,
+ @Nullable @Assisted String topic,
+ TopicValidator topicValidator) {
this.topic = topic;
this.topicEdited = topicEdited;
this.cmUtil = cmUtil;
+ this.topicValidator = topicValidator;
}
@Override
@@ -69,7 +75,7 @@
}
change.setTopic(Strings.emptyToNull(newTopicName));
try {
- update.setTopic(change.getTopic());
+ update.setTopic(change.getTopic(), topicValidator);
} catch (ValidationException ex) {
throw new BadRequestException(ex.getMessage());
}
diff --git a/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java b/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java
index 5be41d4..5b4c3d2 100644
--- a/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java
+++ b/java/com/google/gerrit/server/comment/CommentContextCacheImpl.java
@@ -34,6 +34,7 @@
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.cache.proto.Cache.AllCommentContextProto;
import com.google.gerrit.server.cache.proto.Cache.AllCommentContextProto.CommentContextProto;
@@ -178,15 +179,18 @@
private final ChangeNotes.Factory notesFactory;
private final CommentsUtil commentsUtil;
private final CommentContextLoader.Factory factory;
+ private final DraftCommentsReader draftCommentsReader;
@Inject
Loader(
CommentsUtil commentsUtil,
ChangeNotes.Factory notesFactory,
- CommentContextLoader.Factory factory) {
+ CommentContextLoader.Factory factory,
+ DraftCommentsReader draftCommentsReader) {
this.commentsUtil = commentsUtil;
this.notesFactory = notesFactory;
this.factory = factory;
+ this.draftCommentsReader = draftCommentsReader;
}
/**
@@ -252,7 +256,7 @@
throws IOException {
ChangeNotes notes = notesFactory.createChecked(project, changeId);
List<HumanComment> humanComments = commentsUtil.publishedHumanCommentsByChange(notes);
- List<HumanComment> drafts = commentsUtil.draftByChange(notes);
+ List<HumanComment> drafts = draftCommentsReader.getDraftsByChangeForAllAuthors(notes);
List<HumanComment> allComments =
Streams.concat(humanComments.stream(), drafts.stream()).collect(Collectors.toList());
CommentContextLoader loader = factory.create(project);
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index 4fdbd4a..28baa1a 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -19,7 +19,6 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.CoreDownloadSchemes;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
import com.google.gerrit.server.change.ArchiveFormatInternal;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -41,6 +40,16 @@
*/
@Singleton
public class DownloadConfig {
+ /** Preferred method to download a change. */
+ public enum DownloadCommand {
+ PULL,
+ CHECKOUT,
+ CHERRY_PICK,
+ FORMAT_PATCH,
+ BRANCH,
+ RESET,
+ }
+
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final ImmutableSet<String> downloadSchemes;
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index e0a4269..17d6212 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -81,6 +81,7 @@
import com.google.gerrit.extensions.webui.TopMenu;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.ChangeDraftUpdate;
import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.CreateGroupPermissionSyncer;
import com.google.gerrit.server.DeadlineChecker;
@@ -106,8 +107,8 @@
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.ServiceUserClassifierImpl;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
-import com.google.gerrit.server.account.externalids.ExternalIdCacheModule;
import com.google.gerrit.server.account.externalids.ExternalIdModule;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCacheModule;
import com.google.gerrit.server.account.storage.notedb.AccountNoteDbStorageModule;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.auth.AuthBackend;
@@ -172,6 +173,7 @@
import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.notedb.ChangeDraftNotesUpdate;
import com.google.gerrit.server.notedb.DeleteZombieCommentsRefs;
import com.google.gerrit.server.notedb.NoteDbModule;
import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
@@ -198,6 +200,7 @@
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ConflictsCacheImpl;
import com.google.gerrit.server.quota.QuotaEnforcer;
+import com.google.gerrit.server.restapi.RestModule;
import com.google.gerrit.server.restapi.change.OnPostReview;
import com.google.gerrit.server.restapi.change.SuggestReviewers;
import com.google.gerrit.server.restapi.group.GroupModule;
@@ -219,6 +222,7 @@
import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.validators.AccountActivationValidationListener;
+import com.google.gerrit.server.validators.CustomKeyedValueValidationListener;
import com.google.gerrit.server.validators.GroupCreationValidationListener;
import com.google.gerrit.server.validators.HashtagValidationListener;
import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
@@ -285,6 +289,7 @@
install(new DefaultSubmitRuleModule());
install(new IgnoreSelfApprovalRuleModule());
install(new ReceiveCommitsModule());
+ install(new RestModule());
install(new SshAddressesModule());
install(new FileInfoJsonModule());
install(ThreadLocalRequestContext.module());
@@ -412,6 +417,7 @@
DynamicSet.setOf(binder(), HashtagValidationListener.class);
DynamicSet.setOf(binder(), OutgoingEmailValidationListener.class);
DynamicSet.setOf(binder(), AccountActivationValidationListener.class);
+ DynamicSet.setOf(binder(), CustomKeyedValueValidationListener.class);
DynamicItem.itemOf(binder(), AvatarProvider.class);
DynamicSet.setOf(binder(), LifecycleListener.class);
DynamicSet.setOf(binder(), TopMenu.class);
@@ -474,6 +480,7 @@
bind(CommentValidator.class)
.annotatedWith(Exports.named(CommentCumulativeSizeValidator.class.getSimpleName()))
.to(CommentCumulativeSizeValidator.class);
+ bind(ChangeDraftUpdate.ChangeDraftUpdateFactory.class).to(ChangeDraftNotesUpdate.Factory.class);
DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
diff --git a/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java b/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
index bd07f7d..213cd1c 100644
--- a/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
+++ b/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.config;
+import com.google.gerrit.server.util.ReplicaUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -26,21 +27,15 @@
*/
@Singleton
public final class GerritIsReplicaProvider implements Provider<Boolean> {
- public static final String CONFIG_SECTION = "container";
- public static final String REPLICA_KEY = "replica";
- public static final String DEPRECATED_REPLICA_KEY = "slave";
-
- public final boolean isReplica;
+ private final Config config;
@Inject
public GerritIsReplicaProvider(@GerritServerConfig Config config) {
- this.isReplica =
- config.getBoolean(CONFIG_SECTION, REPLICA_KEY, false)
- || config.getBoolean(CONFIG_SECTION, DEPRECATED_REPLICA_KEY, false);
+ this.config = config;
}
@Override
public Boolean get() {
- return isReplica;
+ return ReplicaUtil.isReplica(config);
}
}
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index e813c09..4c15a7e 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -24,7 +24,6 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
@@ -37,7 +36,6 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
import com.google.gerrit.server.edit.tree.DeleteFileModification;
import com.google.gerrit.server.edit.tree.RenameFileModification;
@@ -105,7 +103,7 @@
private final PatchSetUtil patchSetUtil;
private final ProjectCache projectCache;
private final NoteDbEdits noteDbEdits;
- private final DynamicItem<UrlFormatter> urlFormatter;
+ private final ChangeUtil changeUtil;
@Inject
ChangeEditModifier(
@@ -117,7 +115,7 @@
PatchSetUtil patchSetUtil,
ProjectCache projectCache,
GitReferenceUpdated gitReferenceUpdated,
- DynamicItem<UrlFormatter> urlFormatter) {
+ ChangeUtil changeUtil) {
this.currentUser = currentUser;
this.permissionBackend = permissionBackend;
this.zoneId = gerritIdent.getZoneId();
@@ -125,7 +123,7 @@
this.patchSetUtil = patchSetUtil;
this.projectCache = projectCache;
noteDbEdits = new NoteDbEdits(gitReferenceUpdated, zoneId, indexer, currentUser);
- this.urlFormatter = urlFormatter;
+ this.changeUtil = changeUtil;
}
/**
@@ -520,8 +518,7 @@
"New commit message cannot be same as existing commit message");
}
- ChangeUtil.ensureChangeIdIsCorrect(
- requireChangeId, currentChangeId, newCommitMessage, urlFormatter.get());
+ changeUtil.ensureChangeIdIsCorrect(requireChangeId, currentChangeId, newCommitMessage);
return newCommitMessage;
}
diff --git a/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 814390b..107e987 100644
--- a/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -23,7 +23,7 @@
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -128,7 +128,7 @@
if (batchRefUpdateListeners.isEmpty() && refUpdatedListeners.isEmpty()) {
return;
}
- Set<GitBatchRefUpdateListener.UpdatedRef> updates = new HashSet<>();
+ Set<GitBatchRefUpdateListener.UpdatedRef> updates = new LinkedHashSet<>();
for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
if (cmd.getResult() == ReceiveCommand.Result.OK) {
updates.add(
@@ -254,7 +254,7 @@
public Set<String> getRefNames() {
return updatedRefs.stream()
.map(GitBatchRefUpdateListener.UpdatedRef::getRefName)
- .collect(Collectors.toSet());
+ .collect(Collectors.toCollection(LinkedHashSet::new));
}
@Override
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 6922efb..73aec64 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -143,6 +143,7 @@
private final boolean useContentMerge;
private final boolean useRecursiveMerge;
private final PluggableCommitMessageGenerator commitMessageGenerator;
+ private final ChangeUtil changeUtil;
MergeUtil(
@Provided @GerritServerConfig Config serverConfig,
@@ -150,6 +151,7 @@
@Provided DynamicItem<UrlFormatter> urlFormatter,
@Provided ApprovalsUtil approvalsUtil,
@Provided PluggableCommitMessageGenerator commitMessageGenerator,
+ @Provided ChangeUtil changeUtil,
ProjectState project) {
this(
serverConfig,
@@ -157,6 +159,7 @@
urlFormatter,
approvalsUtil,
commitMessageGenerator,
+ changeUtil,
project,
project.is(BooleanProjectConfig.USE_CONTENT_MERGE));
}
@@ -167,12 +170,14 @@
@Provided DynamicItem<UrlFormatter> urlFormatter,
@Provided ApprovalsUtil approvalsUtil,
@Provided PluggableCommitMessageGenerator commitMessageGenerator,
+ @Provided ChangeUtil changeUtil,
ProjectState project,
boolean useContentMerge) {
this.identifiedUserFactory = identifiedUserFactory;
this.urlFormatter = urlFormatter;
this.approvalsUtil = approvalsUtil;
this.commitMessageGenerator = commitMessageGenerator;
+ this.changeUtil = changeUtil;
this.project = project;
this.useContentMerge = useContentMerge;
this.useRecursiveMerge = useRecursiveMerge(serverConfig);
@@ -281,7 +286,6 @@
return commit;
}
- @SuppressWarnings("resource") // TemporaryBuffer requires calling close before reading.
public static ObjectId mergeWithConflicts(
RevWalk rw,
ObjectInserter ins,
@@ -292,6 +296,21 @@
RevCommit theirs,
Map<String, MergeResult<? extends Sequence>> mergeResults)
throws IOException {
+ return mergeWithConflicts(rw, ins, dc, oursName, ours, theirsName, theirs, mergeResults, false);
+ }
+
+ @SuppressWarnings("resource") // TemporaryBuffer requires calling close before reading.
+ public static ObjectId mergeWithConflicts(
+ RevWalk rw,
+ ObjectInserter ins,
+ DirCache dc,
+ String oursName,
+ RevCommit ours,
+ String theirsName,
+ RevCommit theirs,
+ Map<String, MergeResult<? extends Sequence>> mergeResults,
+ boolean diff3Format)
+ throws IOException {
rw.parseBody(ours);
rw.parseBody(theirs);
String oursMsg = ours.getShortMessage();
@@ -319,7 +338,11 @@
try {
// TODO(dborowitz): Respect inCoreLimit here.
buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
- fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
+ if (diff3Format) {
+ fmt.formatMergeDiff3(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
+ } else {
+ fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
+ }
buf.close(); // Flush file and close for writes, but leave available for reading.
try (InputStream in = buf.openInputStream()) {
@@ -529,7 +552,7 @@
msgbuf.append('\n');
}
- if (ChangeUtil.getChangeIdsFromFooter(n, urlFormatter.get()).isEmpty()) {
+ if (changeUtil.getChangeIdsFromFooter(n).isEmpty()) {
msgbuf.append(FooterConstants.CHANGE_ID.getName());
msgbuf.append(": ");
msgbuf.append(c.getKey().get());
diff --git a/java/com/google/gerrit/server/git/TagCache.java b/java/com/google/gerrit/server/git/TagCache.java
index daf5ea5..d417089 100644
--- a/java/com/google/gerrit/server/git/TagCache.java
+++ b/java/com/google/gerrit/server/git/TagCache.java
@@ -34,7 +34,7 @@
@Override
protected void configure() {
persist(CACHE_NAME, String.class, TagSetHolder.class)
- .version(1)
+ .version(2)
.keySerializer(StringCacheSerializer.INSTANCE)
.valueSerializer(TagSetHolder.Serializer.INSTANCE);
bind(TagCache.class);
diff --git a/java/com/google/gerrit/server/git/TagMatcher.java b/java/com/google/gerrit/server/git/TagMatcher.java
index f003b6f..6edc7f0 100644
--- a/java/com/google/gerrit/server/git/TagMatcher.java
+++ b/java/com/google/gerrit/server/git/TagMatcher.java
@@ -17,15 +17,15 @@
import com.google.gerrit.server.git.TagSet.Tag;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.BitSet;
import java.util.Collection;
import java.util.List;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
+import org.roaringbitmap.RoaringBitmap;
public class TagMatcher {
- final BitSet mask = new BitSet();
+ final RoaringBitmap mask = new RoaringBitmap();
final List<Ref> newRefs = new ArrayList<>();
final List<LostRef> lostRefs = new ArrayList<>();
final TagSetHolder holder;
diff --git a/java/com/google/gerrit/server/git/TagSet.java b/java/com/google/gerrit/server/git/TagSet.java
index 57fcf71..a528c8f 100644
--- a/java/com/google/gerrit/server/git/TagSet.java
+++ b/java/com/google/gerrit/server/git/TagSet.java
@@ -26,8 +26,9 @@
import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.TagProto;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
import com.google.protobuf.ByteString;
+import java.io.DataOutputStream;
import java.io.IOException;
-import java.util.BitSet;
+import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
@@ -42,6 +43,7 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.roaringbitmap.RoaringBitmap;
/**
* Builds a set of tags, and tracks which tags are reachable from which non-tag, non-special refs.
@@ -67,7 +69,7 @@
/**
* refName => ref. CachedRef is a ref that has an integer identity, used for indexing into
- * BitSets.
+ * RoaringBitmaps.
*/
private final Map<String, CachedRef> refs;
@@ -145,7 +147,7 @@
// The reference has not been moved. It can be used as-is.
ObjectId savedObjectId = savedRef.get();
if (currentRef.getObjectId().equals(savedObjectId)) {
- m.mask.set(savedRef.flag);
+ m.mask.add(savedRef.flag);
continue;
}
@@ -162,7 +164,7 @@
if (rw.isMergedInto(savedCommit, currentCommit)) {
// Fast-forward. Safely update the reference in-place.
savedRef.compareAndSet(savedObjectId, currentRef.getObjectId());
- m.mask.set(savedRef.flag);
+ m.mask.add(savedRef.flag);
continue;
}
@@ -176,7 +178,7 @@
RevCommit c;
while ((c = rw.next()) != null) {
Tag tag = tags.get(c);
- if (tag != null && tag.refFlags.get(savedRef.flag)) {
+ if (tag != null && tag.refFlags.contains(savedRef.flag)) {
m.lostRefs.add(new TagMatcher.LostRef(tag, savedRef.flag));
err = true;
}
@@ -184,7 +186,7 @@
if (!err) {
// All of the tags are still reachable. Update in-place.
savedRef.compareAndSet(savedObjectId, currentRef.getObjectId());
- m.mask.set(savedRef.flag);
+ m.mask.add(savedRef.flag);
}
} catch (IOException err) {
@@ -233,7 +235,7 @@
// underlying bit set.
TagCommit c;
while ((c = (TagCommit) rw.next()) != null) {
- BitSet mine = c.refFlags;
+ RoaringBitmap mine = c.refFlags;
int pCnt = c.getParentCount();
for (int pIdx = 0; pIdx < pCnt; pIdx++) {
((TagCommit) c.getParent(pIdx)).refFlags.or(mine);
@@ -257,11 +259,16 @@
proto
.getTagList()
.forEach(
- t ->
- tags.add(
- new Tag(
- idConverter.fromByteString(t.getId()),
- BitSet.valueOf(t.getFlags().asReadOnlyByteBuffer()))));
+ t -> {
+ RoaringBitmap flags = new RoaringBitmap();
+ ByteBuffer in = ByteBuffer.wrap(t.getFlags().toByteArray());
+ try {
+ flags.deserialize(in);
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log();
+ }
+ tags.add(new Tag(idConverter.fromByteString(t.getId()), flags));
+ });
return new TagSet(Project.nameKey(proto.getProjectName()), refs, tags);
}
@@ -277,12 +284,20 @@
.setFlag(cr.flag)
.build()));
tags.forEach(
- t ->
- b.addTag(
- TagProto.newBuilder()
- .setId(idConverter.toByteString(t))
- .setFlags(ByteString.copyFrom(t.refFlags.toByteArray()))
- .build()));
+ t -> {
+ t.refFlags.runOptimize();
+ ByteString.Output out = ByteString.newOutput(t.refFlags.serializedSizeInBytes());
+ try {
+ t.refFlags.serialize(new DataOutputStream(out));
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log();
+ }
+ b.addTag(
+ TagProto.newBuilder()
+ .setId(idConverter.toByteString(t))
+ .setFlags(out.toByteString())
+ .build());
+ });
return b.build();
}
@@ -328,8 +343,8 @@
refs.put(newRef.getName(), new CachedRef(newRef, newFlag));
for (Tag tag : tags) {
- if (tag.refFlags.get(srcFlag)) {
- tag.refFlags.set(newFlag);
+ if (tag.refFlags.contains(srcFlag)) {
+ tag.refFlags.add(newFlag);
}
}
}
@@ -341,7 +356,7 @@
refs.putAll(old.refs);
for (Tag srcTag : old.tags) {
- BitSet mine = new BitSet();
+ RoaringBitmap mine = new RoaringBitmap();
mine.or(srcTag.refFlags);
tags.add(new Tag(srcTag, mine));
}
@@ -349,7 +364,7 @@
for (TagMatcher.LostRef lost : m.lostRefs) {
Tag mine = tags.get(lost.tag);
if (mine != null) {
- mine.refFlags.clear(lost.flag);
+ mine.refFlags.remove(lost.flag);
}
}
}
@@ -361,14 +376,14 @@
}
if (!tags.contains(id)) {
- BitSet flags;
+ RoaringBitmap flags;
try {
flags = ((TagCommit) rw.parseCommit(id)).refFlags;
} catch (IncorrectObjectTypeException notCommit) {
- flags = new BitSet();
+ flags = new RoaringBitmap();
} catch (IOException e) {
logger.atWarning().withCause(e).log("Error on %s of %s", ref.getName(), projectName);
- flags = new BitSet();
+ flags = new RoaringBitmap();
}
tags.add(new Tag(id, flags));
}
@@ -380,7 +395,7 @@
rw.markStart(commit);
int flag = refs.size();
- commit.refFlags.set(flag);
+ commit.refFlags.add(flag);
refs.put(ref.getName(), new CachedRef(ref, flag));
} catch (IncorrectObjectTypeException notCommit) {
// No need to spam the logs.
@@ -414,16 +429,16 @@
static final class Tag extends ObjectIdOwnerMap.Entry {
// a RefCache.flag => isVisible map. This reference is aliased to the
- // bitset in TagCommit.refFlags.
- @VisibleForTesting final BitSet refFlags;
+ // RoaringBitmap in TagCommit.refFlags.
+ @VisibleForTesting final RoaringBitmap refFlags;
- Tag(AnyObjectId id, BitSet flags) {
+ Tag(AnyObjectId id, RoaringBitmap flags) {
super(id);
this.refFlags = flags;
}
- boolean has(BitSet mask) {
- return refFlags.intersects(mask);
+ boolean has(RoaringBitmap mask) {
+ return RoaringBitmap.intersects(refFlags, mask);
}
@Override
@@ -431,7 +446,7 @@
return MoreObjects.toStringHelper(this).addValue(name()).add("refFlags", refFlags).toString();
}
}
- /** A ref along with its index into BitSet. */
+ /** A ref along with its index into RoaringBitmap. */
@VisibleForTesting
static final class CachedRef extends AtomicReference<ObjectId> {
private static final long serialVersionUID = 1L;
@@ -472,11 +487,11 @@
// TODO(hanwen): this would be better named as CommitWithReachability, as it also holds non-tags.
private static final class TagCommit extends RevCommit {
/** CachedRef.flag => isVisible, indicating if this commit is reachable from the ref. */
- final BitSet refFlags;
+ final RoaringBitmap refFlags;
TagCommit(AnyObjectId id) {
super(id);
- refFlags = new BitSet();
+ refFlags = new RoaringBitmap();
}
}
}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 4d47686..b0e1e38 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -109,9 +109,9 @@
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.CancellationMetrics;
import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CreateGroupPermissionSyncer;
import com.google.gerrit.server.DeadlineChecker;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InvalidDeadlineException;
import com.google.gerrit.server.PatchSetUtil;
@@ -133,7 +133,6 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.ProjectConfigEntry;
-import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.edit.ChangeEditUtil;
import com.google.gerrit.server.git.BanCommit;
@@ -372,8 +371,9 @@
private final ChangeInserter.Factory changeInserterFactory;
private final ChangeNotes.Factory notesFactory;
private final ChangeReportFormatter changeFormatter;
+ private final ChangeUtil changeUtil;
private final CmdLineParser.Factory optionParserFactory;
- private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
private final PluginSetContext<CommentValidator> commentValidators;
private final BranchCommitValidator.Factory commitValidatorFactory;
private final Config config;
@@ -408,7 +408,6 @@
private final ProjectConfig.Factory projectConfigFactory;
private final SetPrivateOp.Factory setPrivateOpFactory;
private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
- private final DynamicItem<UrlFormatter> urlFormatter;
private final AutoMerger autoMerger;
// Assisted injected fields.
@@ -461,8 +460,9 @@
ChangeInserter.Factory changeInserterFactory,
ChangeNotes.Factory notesFactory,
DynamicItem<ChangeReportFormatter> changeFormatterProvider,
+ ChangeUtil changeUtil,
CmdLineParser.Factory optionParserFactory,
- CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
BranchCommitValidator.Factory commitValidatorFactory,
CreateGroupPermissionSyncer createGroupPermissionSyncer,
CreateRefControl createRefControl,
@@ -496,7 +496,6 @@
TagCache tagCache,
SetPrivateOp.Factory setPrivateOpFactory,
ReplyAttentionSetUpdates replyAttentionSetUpdates,
- DynamicItem<UrlFormatter> urlFormatter,
AutoMerger autoMerger,
@Assisted ProjectState projectState,
@Assisted IdentifiedUser user,
@@ -511,8 +510,9 @@
this.batchUpdateFactory = batchUpdateFactory;
this.cancellationMetrics = cancellationMetrics;
this.changeFormatter = changeFormatterProvider.get();
+ this.changeUtil = changeUtil;
this.changeInserterFactory = changeInserterFactory;
- this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.commentValidators = commentValidators;
this.commitValidatorFactory = commitValidatorFactory;
this.config = config;
@@ -551,7 +551,6 @@
this.projectConfigFactory = projectConfigFactory;
this.setPrivateOpFactory = setPrivateOpFactory;
this.replyAttentionSetUpdates = replyAttentionSetUpdates;
- this.urlFormatter = urlFormatter;
this.autoMerger = autoMerger;
// Assisted injected fields.
@@ -1116,7 +1115,8 @@
continue;
}
List<HumanComment> drafts =
- commentsUtil.draftByChangeAuthor(changeNotes.get(), user.getAccountId());
+ draftCommentsReader.getDraftsByChangeAndDraftAuthor(
+ changeNotes.get(), user.getAccountId());
if (drafts.isEmpty()) {
// If no comments, attention set shouldn't update since the user
// didn't reply.
@@ -2255,7 +2255,7 @@
if (magicBranch != null && magicBranch.shouldPublishComments()) {
List<HumanComment> drafts =
- commentsUtil.draftByChangeAuthor(
+ draftCommentsReader.getDraftsByChangeAndDraftAuthor(
notesFactory.createChecked(change), user.getAccountId());
ImmutableList<CommentForValidation> draftsForValidation =
drafts.stream()
@@ -2294,7 +2294,7 @@
} catch (IOException e) {
throw new StorageException("Can't parse commit", e);
}
- List<String> idList = ChangeUtil.getChangeIdsFromFooter(create.commit, urlFormatter.get());
+ List<String> idList = changeUtil.getChangeIdsFromFooter(create.commit);
if (idList.isEmpty()) {
messages.add(
@@ -2369,7 +2369,7 @@
}
}
- List<String> idList = ChangeUtil.getChangeIdsFromFooter(c, urlFormatter.get());
+ List<String> idList = changeUtil.getChangeIdsFromFooter(c);
if (!idList.isEmpty()) {
pending.put(c, lookupByChangeKey(c, Change.key(idList.get(idList.size() - 1).trim())));
} else {
@@ -3492,8 +3492,7 @@
}
}
- for (String changeId :
- ChangeUtil.getChangeIdsFromFooter(c, urlFormatter.get())) {
+ for (String changeId : changeUtil.getChangeIdsFromFooter(c)) {
if (changeDataByKey == null) {
changeDataByKey =
retryHelper
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 0e17342..7cc843b 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -40,7 +40,6 @@
import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -61,11 +60,11 @@
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModificationList;
import com.google.gerrit.server.change.ReviewerOp;
import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.git.MergedByPushOp;
import com.google.gerrit.server.git.receive.ReceiveCommits.MagicBranchInput;
+import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -134,7 +133,8 @@
private final PatchSetUtil psUtil;
private final ProjectCache projectCache;
private final ReviewerModifier reviewerModifier;
- private final DynamicItem<UrlFormatter> urlFormatter;
+ private final ChangeUtil changeUtil;
+ private final TopicValidator topicValidator;
private final ProjectState projectState;
private final Change change;
@@ -179,7 +179,8 @@
ProjectCache projectCache,
EmailNewPatchSet.Factory emailNewPatchSetFactory,
ReviewerModifier reviewerModifier,
- DynamicItem<UrlFormatter> urlFormatter,
+ ChangeUtil changeUtil,
+ TopicValidator topicValidator,
@Assisted ProjectState projectState,
@Assisted Change change,
@Assisted boolean checkMergedInto,
@@ -207,7 +208,8 @@
this.projectCache = projectCache;
this.emailNewPatchSetFactory = emailNewPatchSetFactory;
this.reviewerModifier = reviewerModifier;
- this.urlFormatter = urlFormatter;
+ this.changeUtil = changeUtil;
+ this.topicValidator = topicValidator;
this.projectState = projectState;
this.change = change;
@@ -287,7 +289,7 @@
}
if (magicBranch.topic != null && !magicBranch.topic.equals(ctx.getChange().getTopic())) {
try {
- update.setTopic(magicBranch.topic);
+ update.setTopic(magicBranch.topic, topicValidator);
} catch (ValidationException ex) {
throw new BadRequestException(ex.getMessage());
}
@@ -496,7 +498,7 @@
change.setStatus(Change.Status.NEW);
change.setCurrentPatchSet(info);
- List<String> idList = ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter.get());
+ List<String> idList = changeUtil.getChangeIdsFromFooter(commit);
change.setKey(Change.key(idList.get(idList.size() - 1).trim()));
}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 999f810..cd1e2ee 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -38,6 +38,7 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
@@ -105,10 +106,12 @@
private final AllProjectsName allProjects;
private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
private final AccountValidator accountValidator;
+ private final AccountCache accountCache;
private final ProjectCache projectCache;
private final ProjectConfig.Factory projectConfigFactory;
private final DiffOperations diffOperations;
private final Config config;
+ private final ChangeUtil changeUtil;
@Inject
Factory(
@@ -121,9 +124,11 @@
AllProjectsName allProjects,
ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
AccountValidator accountValidator,
+ AccountCache accountCache,
ProjectCache projectCache,
ProjectConfig.Factory projectConfigFactory,
- DiffOperations diffOperations) {
+ DiffOperations diffOperations,
+ ChangeUtil changeUtil) {
this.gerritIdent = gerritIdent;
this.urlFormatter = urlFormatter;
this.config = config;
@@ -133,9 +138,11 @@
this.allProjects = allProjects;
this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
this.accountValidator = accountValidator;
+ this.accountCache = accountCache;
this.projectCache = projectCache;
this.projectConfigFactory = projectConfigFactory;
this.diffOperations = diffOperations;
+ this.changeUtil = changeUtil;
}
public CommitValidators forReceiveCommits(
@@ -156,16 +163,16 @@
.add(new ProjectStateValidationListener(projectState))
.add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
.add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
- .add(new FileCountValidator(repoManager, config))
+ .add(new FileCountValidator(repoManager, config, urlFormatter.get()))
.add(new CommitterUploaderValidator(user, perm, urlFormatter.get()))
.add(new SignedOffByValidator(user, perm, projectState))
.add(
new ChangeIdValidator(
- projectState, user, urlFormatter.get(), config, sshInfo, change))
+ changeUtil, projectState, user, urlFormatter.get(), config, sshInfo, change))
.add(new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects))
.add(new BannedCommitsValidator(rejectCommits))
.add(new PluginCommitValidationListener(pluginValidators, skipValidation))
- .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker))
+ .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker, accountCache))
.add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
.add(new GroupCommitValidator(allUsers))
.add(new LabelConfigValidator(diffOperations));
@@ -188,14 +195,14 @@
.add(new ProjectStateValidationListener(projectState))
.add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
.add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
- .add(new FileCountValidator(repoManager, config))
+ .add(new FileCountValidator(repoManager, config, urlFormatter.get()))
.add(new SignedOffByValidator(user, perm, projectState))
.add(
new ChangeIdValidator(
- projectState, user, urlFormatter.get(), config, sshInfo, change))
+ changeUtil, projectState, user, urlFormatter.get(), config, sshInfo, change))
.add(new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects))
.add(new PluginCommitValidationListener(pluginValidators))
- .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker))
+ .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker, accountCache))
.add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
.add(new GroupCommitValidator(allUsers))
.add(new LabelConfigValidator(diffOperations));
@@ -280,6 +287,7 @@
private static final Pattern CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
+ private final ChangeUtil changeUtil;
private final ProjectState projectState;
private final UrlFormatter urlFormatter;
private final String installCommitMsgHookCommand;
@@ -288,12 +296,14 @@
private final Change change;
public ChangeIdValidator(
+ ChangeUtil changeUtil,
ProjectState projectState,
IdentifiedUser user,
UrlFormatter urlFormatter,
Config config,
SshInfo sshInfo,
Change change) {
+ this.changeUtil = changeUtil;
this.projectState = projectState;
this.user = user;
this.urlFormatter = urlFormatter;
@@ -310,7 +320,7 @@
}
RevCommit commit = receiveEvent.commit;
List<CommitValidationMessage> messages = new ArrayList<>();
- List<String> idList = ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter);
+ List<String> idList = changeUtil.getChangeIdsFromFooter(commit);
if (idList.isEmpty()) {
String shortMsg = commit.getShortMessage();
@@ -425,11 +435,15 @@
/** Limits the number of files per change. */
private static class FileCountValidator implements CommitValidationListener {
+ private static final int FILE_COUNT_WARNING_THRESHOLD = 10_000;
+
private final GitRepositoryManager repoManager;
private final int maxFileCount;
+ private final UrlFormatter urlFormatter;
- FileCountValidator(GitRepositoryManager repoManager, Config config) {
+ FileCountValidator(GitRepositoryManager repoManager, Config config, UrlFormatter urlFormatter) {
this.repoManager = repoManager;
+ this.urlFormatter = urlFormatter;
maxFileCount = config.getInt("change", null, "maxFiles", 100_000);
}
@@ -457,6 +471,13 @@
"Exceeding maximum number of files per change (%d > %d)",
changedFiles, maxFileCount));
}
+ if (changedFiles > FILE_COUNT_WARNING_THRESHOLD) {
+ String host = getGerritHost(urlFormatter.getWebUrl().orElse(null));
+ String project = receiveEvent.project.getNameKey().get();
+ logger.atWarning().log(
+ "Warning: Change with %d files on host %s, project %s, ref %s",
+ changedFiles, host, project, refName);
+ }
} catch (IOException e) {
// This happens e.g. for cherrypicks.
if (!receiveEvent.command.getRefName().startsWith(REFS_CHANGES)) {
@@ -809,12 +830,16 @@
/** Validates updates to refs/meta/external-ids. */
public static class ExternalIdUpdateListener implements CommitValidationListener {
private final AllUsersName allUsers;
+ private final AccountCache accountCache;
private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
public ExternalIdUpdateListener(
- AllUsersName allUsers, ExternalIdsConsistencyChecker externalIdsConsistencyChecker) {
+ AllUsersName allUsers,
+ ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
+ AccountCache accountCache) {
this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
this.allUsers = allUsers;
+ this.accountCache = accountCache;
}
@Override
@@ -824,7 +849,7 @@
&& RefNames.REFS_EXTERNAL_IDS.equals(receiveEvent.refName)) {
try {
List<ConsistencyProblemInfo> problems =
- externalIdsConsistencyChecker.check(receiveEvent.commit);
+ externalIdsConsistencyChecker.check(accountCache, receiveEvent.commit);
List<CommitValidationMessage> msgs =
problems.stream()
.map(
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 40ce671..514dee1 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -36,6 +36,7 @@
import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.group.db.GroupConfig;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -343,10 +344,12 @@
}
private final AllUsersName allUsersName;
+ private final ChangeData.Factory changeDataFactory;
@Inject
- public GroupMergeValidator(AllUsersName allUsersName) {
+ public GroupMergeValidator(AllUsersName allUsersName, ChangeData.Factory changeDataFactory) {
this.allUsersName = allUsersName;
+ this.changeDataFactory = changeDataFactory;
}
@Override
@@ -365,7 +368,29 @@
return;
}
- throw new MergeValidationException("group update not allowed");
+ // Update to group files is not supported because there are no validations
+ // on the changes being done to these files, without which the group data
+ // might get corrupted. Thus don't allow merges into All-Users group refs
+ // which updates group files (i.e., group.config, members and subgroups).
+ // But it is still useful to allow users to update files apart from group
+ // files. For example, users can upload named destinations into group refs.
+ ChangeData cd =
+ changeDataFactory.create(destProject.getProject().getNameKey(), patchSetId.changeId());
+ try {
+ if (cd.currentFilePaths().contains(GroupConfig.GROUP_CONFIG_FILE)
+ || cd.currentFilePaths().contains(GroupConfig.MEMBERS_FILE)
+ || cd.currentFilePaths().contains(GroupConfig.SUBGROUPS_FILE)) {
+ throw new MergeValidationException(
+ String.format(
+ "update to group files (%s, %s, %s) not allowed",
+ GroupConfig.GROUP_CONFIG_FILE,
+ GroupConfig.MEMBERS_FILE,
+ GroupConfig.SUBGROUPS_FILE));
+ }
+ } catch (StorageException e) {
+ logger.atSevere().withCause(e).log("Cannot validate group update");
+ throw new MergeValidationException("group validation unavailable", e);
+ }
}
}
diff --git a/java/com/google/gerrit/server/git/validators/TopicValidator.java b/java/com/google/gerrit/server/git/validators/TopicValidator.java
new file mode 100644
index 0000000..46c56f3
--- /dev/null
+++ b/java/com/google/gerrit/server/git/validators/TopicValidator.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2023 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.git.validators;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+
+/** Validator for topic changes. */
+@Singleton
+public class TopicValidator {
+
+ private final Provider<InternalChangeQuery> queryProvider;
+ private final int topicLimit;
+
+ @Inject
+ TopicValidator(
+ @GerritServerConfig Config serverConfig, Provider<InternalChangeQuery> queryProvider) {
+ this.queryProvider = queryProvider;
+ int configuredLimit = serverConfig.getInt("change", "topicLimit", 5_000);
+ this.topicLimit = configuredLimit > 0 ? configuredLimit : Integer.MAX_VALUE;
+ }
+
+ public void validateSize(@Nullable String topic) throws ValidationException {
+ if (Strings.isNullOrEmpty(topic)) {
+ return;
+ }
+ int topicSize = queryProvider.get().noFields().byTopicOpen(topic).size();
+ if (topicSize >= topicLimit) {
+ throw new ValidationException(
+ String.format(
+ "Topic '%s' already contains maximum number of allowed changes per 'topicLimit'"
+ + " server config value %d.",
+ topic, topicLimit));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index 4f2c049..3470a6c 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -90,8 +90,8 @@
*/
public class GroupConfig extends VersionedMetaData {
@VisibleForTesting public static final String GROUP_CONFIG_FILE = "group.config";
- @VisibleForTesting static final String MEMBERS_FILE = "members";
- @VisibleForTesting static final String SUBGROUPS_FILE = "subgroups";
+ @VisibleForTesting public static final String MEMBERS_FILE = "members";
+ @VisibleForTesting public static final String SUBGROUPS_FILE = "subgroups";
private static final Pattern LINE_SEPARATOR_PATTERN = Pattern.compile("\\R");
/**
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 7ee484b..4f411a2 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -21,6 +21,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.ListenableFuture;
@@ -39,7 +40,6 @@
import com.google.gerrit.server.index.OnlineReindexMode;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
-import com.google.gerrit.server.notedb.ChangeNotes.Factory.ScanResult;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
@@ -50,6 +50,7 @@
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Repository;
@@ -108,14 +109,19 @@
public abstract int slices();
- public abstract ScanResult scanResult();
+ public abstract ImmutableMap<Change.Id, ObjectId> metaIdByChange();
- private static ProjectSlice create(Project.NameKey name, int slice, int slices, ScanResult sr) {
- return new AutoValue_AllChangesIndexer_ProjectSlice(name, slice, slices, sr);
+ private static ProjectSlice create(
+ Project.NameKey name,
+ int slice,
+ int slices,
+ ImmutableMap<Change.Id, ObjectId> metaIdByChange) {
+ return new AutoValue_AllChangesIndexer_ProjectSlice(name, slice, slices, metaIdByChange);
}
- private static ProjectSlice oneSlice(Project.NameKey name, ScanResult sr) {
- return new AutoValue_AllChangesIndexer_ProjectSlice(name, 0, 1, sr);
+ private static ProjectSlice oneSlice(
+ Project.NameKey name, ImmutableMap<Change.Id, ObjectId> metaIdByChange) {
+ return new AutoValue_AllChangesIndexer_ProjectSlice(name, 0, 1, metaIdByChange);
}
}
@@ -227,7 +233,7 @@
// we don't have concrete proof that improving packfile locality would help.
notesFactory
.scan(
- projectSlice.scanResult(),
+ projectSlice.metaIdByChange(),
projectSlice.name(),
id -> (id.get() % projectSlice.slices()) == projectSlice.slice())
.forEach(r -> index(r));
@@ -339,8 +345,9 @@
@Override
public Void call() throws IOException {
try (Repository repo = repoManager.openRepository(name)) {
- ScanResult sr = ChangeNotes.Factory.scanChangeIds(repo);
- int size = sr.all().size();
+ ImmutableMap<Change.Id, ObjectId> metaIdByChange =
+ ChangeNotes.Factory.scanChangeIds(repo);
+ int size = metaIdByChange.size();
if (size > 0) {
changeCount.addAndGet(size);
int slices = 1 + (size - 1) / PROJECT_SLICE_MAX_REFS;
@@ -353,7 +360,7 @@
projTask.updateTotal(slices);
for (int slice = 0; slice < slices; slice++) {
- ProjectSlice projectSlice = ProjectSlice.create(name, slice, slices, sr);
+ ProjectSlice projectSlice = ProjectSlice.create(name, slice, slices, metaIdByChange);
ListenableFuture<?> future =
executor.submit(
reindexProjectSlice(
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 7057ff7..d9d7a90 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -16,7 +16,6 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -24,6 +23,7 @@
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
+import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.HashBasedTable;
@@ -36,6 +36,7 @@
import com.google.common.collect.Table;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.Files;
+import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import com.google.common.reflect.TypeToken;
import com.google.gerrit.common.Nullable;
@@ -63,7 +64,6 @@
import com.google.gerrit.proto.Entities;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.cache.proto.Cache;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
@@ -81,6 +81,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -1267,21 +1268,19 @@
public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec COMMENTBY_SPEC =
COMMENTBY_FIELD.integer(ChangeQueryBuilder.FIELD_COMMENTBY);
- /** Star labels on this change in the format: <account-id>:<label> */
+ /** Star labels on this change in the format: <account-id> */
public static final IndexedField<ChangeData, Iterable<String>> STAR_FIELD =
IndexedField.<ChangeData>iterableStringBuilder("Star")
.stored()
.build(
cd ->
Iterables.transform(
- cd.stars().entries(),
- e ->
- StarredChangesUtil.StarField.create(e.getKey(), e.getValue()).toString()),
+ cd.stars(), accountId -> StarField.create(accountId).toString()),
(cd, field) ->
cd.setStars(
StreamSupport.stream(field.spliterator(), false)
- .map(f -> StarredChangesUtil.StarField.parse(f))
- .collect(toImmutableListMultimap(e -> e.accountId(), e -> e.label()))));
+ .map(f -> StarField.parse(f).accountId())
+ .collect(toImmutableList())));
public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec STAR_SPEC =
STAR_FIELD.exact(ChangeQueryBuilder.FIELD_STAR);
@@ -1289,7 +1288,7 @@
/** Users that have starred the change with any label. */
public static final IndexedField<ChangeData, Iterable<Integer>> STARBY_FIELD =
IndexedField.<ChangeData>iterableIntegerBuilder("StarBy")
- .build(cd -> Iterables.transform(cd.stars().keySet(), Account.Id::get));
+ .build(cd -> Iterables.transform(cd.stars(), Account.Id::get));
public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec STARBY_SPEC =
STARBY_FIELD.integer(ChangeQueryBuilder.FIELD_STARBY);
@@ -1710,6 +1709,30 @@
public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec REF_STATE_PATTERN_SPEC =
REF_STATE_PATTERN_FIELD.storedOnly("ref_state_pattern");
+ public static final IndexedField<ChangeData, Iterable<String>> CUSTOM_KEYED_VALUES_FIELD =
+ IndexedField.<ChangeData>iterableStringBuilder("CustomKeyedValues")
+ .stored()
+ .build(
+ cd ->
+ cd.customKeyedValues().entrySet().stream()
+ .map(e -> e.getKey() + "=" + e.getValue())
+ .collect(toList()),
+ (cd, field) -> {
+ Map<String, String> ckv = new HashMap<>();
+ for (String entry : field) {
+ int splitPoint = entry.indexOf('=');
+ if (splitPoint < 0) {
+ continue;
+ }
+ ckv.put(entry.substring(0, splitPoint), entry.substring(splitPoint + 1));
+ }
+ cd.setCustomKeyedValues(ckv);
+ });
+
+ public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec
+ CUSTOM_KEYED_VALUES_SPEC =
+ CUSTOM_KEYED_VALUES_FIELD.prefix(ChangeQueryBuilder.FIELD_CUSTOM_KEYED_VALUES);
+
@Nullable
private static String getTopic(ChangeData cd) {
Change c = cd.change();
@@ -1792,4 +1815,45 @@
}
return str;
}
+
+ @AutoValue
+ abstract static class StarField {
+ private static final String SEPARATOR = ":";
+
+ @Nullable
+ static StarField parse(String s) {
+ Integer id;
+ int p = s.indexOf(SEPARATOR);
+ if (p >= 0) {
+ id = Ints.tryParse(s.substring(0, p));
+ } else {
+ // NOTE: This code branch should not be removed. This code is used internally by Google and
+ // must not be changed without approval from a Google contributor. In
+ // 992877d06d3492f78a3b189eb5579ddb86b9f0da we accidentally changed index writing to write
+ // <account_id> instead of <account_id>:star. As some servers have picked that up and wrote
+ // index entries with the short format, we should keep support its parsing.
+ id = Ints.tryParse(s);
+ }
+ if (id == null) {
+ return null;
+ }
+ return create(Account.id(id));
+ }
+
+ static StarField create(Account.Id accountId) {
+ return new AutoValue_ChangeField_StarField(accountId);
+ }
+
+ public abstract Account.Id accountId();
+
+ @Override
+ public final String toString() {
+ // NOTE: The ":star" addition is used internally by Google and must not be removed without
+ // approval from a Google contributor. This method is used for writing change index data.
+ // Historically, we supported different kinds of labels, which were stored in this
+ // format, with "star" being the only label in use. This label addition stayed in order to
+ // keep the index format consistent while removing the star-label support.
+ return accountId() + SEPARATOR + "star";
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 6f2bfdd..5474e6b 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -248,7 +248,14 @@
.build();
/** Upgrade Lucene to 8.x requires reindexing. */
- static final Schema<ChangeData> V83 = schema(V82);
+ @Deprecated static final Schema<ChangeData> V83 = schema(V82);
+
+ static final Schema<ChangeData> V84 =
+ new Schema.Builder<ChangeData>()
+ .add(V83)
+ .addIndexedFields(ChangeField.CUSTOM_KEYED_VALUES_FIELD)
+ .addSearchSpecs(ChangeField.CUSTOM_KEYED_VALUES_SPEC)
+ .build();
/**
* Name of the change index to be used when contacting index backends or loading configurations.
diff --git a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 8f5e36e..d2c3820 100644
--- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -157,6 +157,12 @@
@Override
public boolean match(ChangeData cd) {
+ if (index.getIndexFilter().isPresent()) {
+ // Evaluate the filter. If we pass the filter, then evaluate everything else.
+ if (!index.getIndexFilter().get().match(cd)) {
+ return false;
+ }
+ }
Predicate<ChangeData> pred = getChild(0);
if (source != null && fromSource.get(cd) == source && postIndexMatch(pred, cd)) {
return true;
diff --git a/java/com/google/gerrit/server/mail/send/AbandonedChangeEmailDecorator.java b/java/com/google/gerrit/server/mail/send/AbandonedChangeEmailDecorator.java
index 3ec2b35..a1eede0 100644
--- a/java/com/google/gerrit/server/mail/send/AbandonedChangeEmailDecorator.java
+++ b/java/com/google/gerrit/server/mail/send/AbandonedChangeEmailDecorator.java
@@ -19,8 +19,8 @@
/** Send notice about a change being abandoned by its owner. */
public class AbandonedChangeEmailDecorator implements ChangeEmail.ChangeEmailDecorator {
- private ChangeEmail changeEmail;
- private OutgoingEmail email;
+ protected ChangeEmail changeEmail;
+ protected OutgoingEmail email;
@Override
public void init(OutgoingEmail email, ChangeEmail changeEmail) {
diff --git a/java/com/google/gerrit/server/mail/send/AttentionSetChangeEmailDecoratorImpl.java b/java/com/google/gerrit/server/mail/send/AttentionSetChangeEmailDecoratorImpl.java
index 2db75e8..6d09a2b 100644
--- a/java/com/google/gerrit/server/mail/send/AttentionSetChangeEmailDecoratorImpl.java
+++ b/java/com/google/gerrit/server/mail/send/AttentionSetChangeEmailDecoratorImpl.java
@@ -18,14 +18,13 @@
import com.google.gerrit.extensions.api.changes.RecipientType;
/** Base class for Attention Set email senders */
-public final class AttentionSetChangeEmailDecoratorImpl
- implements AttentionSetChangeEmailDecorator {
- private OutgoingEmail email;
- private ChangeEmail changeEmail;
+public class AttentionSetChangeEmailDecoratorImpl implements AttentionSetChangeEmailDecorator {
+ protected OutgoingEmail email;
+ protected ChangeEmail changeEmail;
- private Account.Id attentionSetUser;
- private String reason;
- private AttentionSetChange attentionSetChange;
+ protected Account.Id attentionSetUser;
+ protected String reason;
+ protected AttentionSetChange attentionSetChange;
@Override
public void setAttentionSetUser(Account.Id attentionSetUser) {
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index ea31fc0..b15a506 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -109,6 +109,12 @@
/** Get the patch list corresponding to this patch set. */
Map<String, FileDiffOutput> listModifiedFiles();
+ /** Get the number of added lines in a change. */
+ int getInsertionsCount();
+
+ /** Get the number of deleted lines in a change. */
+ int getDeletionsCount();
+
/** Get the project entity the change is in; null if its been deleted. */
ProjectState getProjectState();
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java b/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java
index bf6ed0c..4ecbd52 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmailImpl.java
@@ -19,7 +19,6 @@
import com.google.auto.factory.AutoFactory;
import com.google.auto.factory.Provided;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -40,7 +39,6 @@
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.mail.MailHeader;
-import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
import com.google.gerrit.server.mail.send.ProjectWatch.Watchers.WatcherList;
@@ -58,9 +56,9 @@
import java.io.IOException;
import java.text.MessageFormat;
import java.time.Instant;
-import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -77,22 +75,22 @@
/** Populates an email for change related notifications. */
@AutoFactory
-public final class ChangeEmailImpl implements ChangeEmail {
+public class ChangeEmailImpl implements ChangeEmail {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// Available after construction
- private final EmailArguments args;
- private final Set<Account.Id> currentAttentionSet;
- private final Change change;
- private final ChangeData changeData;
- private final BranchNameKey branch;
- private final ChangeEmailDecorator changeEmailDecorator;
+ protected final EmailArguments args;
+ protected final Set<Account.Id> currentAttentionSet;
+ protected final Change change;
+ protected final ChangeData changeData;
+ protected final BranchNameKey branch;
+ protected final ChangeEmailDecorator changeEmailDecorator;
// Available after init or after being explicitly set.
- private OutgoingEmail email;
- private ListMultimap<Account.Id, String> stars;
- private PatchSet patchSet;
- private PatchSetInfo patchSetInfo;
+ protected OutgoingEmail email;
+ private List<Account.Id> stars;
+ protected PatchSet patchSet;
+ protected PatchSetInfo patchSetInfo;
private String changeMessage;
private String changeMessageThreadId;
private Instant timestamp;
@@ -121,20 +119,6 @@
this.changeEmailDecorator = changeEmailDecorator;
}
- public ChangeEmailImpl(
- @Provided EmailArguments args,
- ChangeData changeData,
- ChangeEmailDecorator changeEmailDecorator) {
- this.args = args;
- this.changeData = changeData;
- change = changeData.change();
- emailOnlyAuthors = false;
- emailOnlyAttentionSetIfEnabled = true;
- currentAttentionSet = getAttentionSet();
- branch = changeData.change().getDest();
- this.changeEmailDecorator = changeEmailDecorator;
- }
-
@Override
public void markAsReply() {
isThreadReply = true;
@@ -264,11 +248,12 @@
}
}
- private void setChangeSubjectHeader() {
+ protected void setChangeSubjectHeader() {
email.setHeader(FieldName.SUBJECT, email.textTemplate("ChangeSubject"));
}
- private int getInsertionsCount() {
+ @Override
+ public int getInsertionsCount() {
return listModifiedFiles().entrySet().stream()
.filter(e -> !Patch.COMMIT_MSG.equals(e.getKey()))
.map(Map.Entry::getValue)
@@ -276,7 +261,8 @@
.reduce(0, Integer::sum);
}
- private int getDeletionsCount() {
+ @Override
+ public int getDeletionsCount() {
return listModifiedFiles().values().stream()
.map(FileDiffOutput::deletions)
.reduce(0, Integer::sum);
@@ -288,7 +274,7 @@
* clickthroughs where the link came from.
*/
@Nullable
- private String getChangeUrl() {
+ protected String getChangeUrl() {
return args.urlFormatter
.get()
.getChangeViewUrl(change.getProject(), change.getId())
@@ -297,7 +283,7 @@
}
/** Sets headers for conversation grouping */
- private void setThreadHeaders() {
+ protected void setThreadHeaders() {
if (isThreadReply) {
email.setHeader("In-Reply-To", changeMessageThreadId);
}
@@ -314,7 +300,7 @@
}
/** Create the change message and the affected file list. */
- private String getChangeDetail() {
+ protected String getChangeDetail() {
try {
StringBuilder detail = new StringBuilder();
@@ -414,11 +400,7 @@
return;
}
- for (Map.Entry<Account.Id, Collection<String>> e : stars.asMap().entrySet()) {
- if (e.getValue().contains(StarredChangesUtil.DEFAULT_LABEL)) {
- email.addByAccountId(RecipientType.BCC, e.getKey());
- }
- }
+ stars.forEach(accountId -> email.addByAccountId(RecipientType.BCC, accountId));
}
/** Include users and groups that want notification of events. */
@@ -631,12 +613,10 @@
for (Account.Id attentionUser : currentAttentionSet) {
email.addFooter(MailHeader.ATTENTION.withDelimiter() + email.getNameEmailFor(attentionUser));
}
- if (!currentAttentionSet.isEmpty()) {
- // We need names rather than account ids / emails to make it user readable.
- email.addSoyParam(
- "attentionSet",
- currentAttentionSet.stream().map(email::getNameFor).sorted().collect(toImmutableList()));
- }
+ // We need names rather than account ids / emails to make it user readable.
+ email.addSoyParam(
+ "attentionSet",
+ currentAttentionSet.stream().map(email::getNameFor).sorted().collect(toImmutableList()));
setChangeSubjectHeader();
if (email.getNotify().handling().equals(NotifyHandling.OWNER_REVIEWERS)
@@ -666,14 +646,14 @@
* A shortened subject is the subject limited to 72 characters, with an ellipsis if it exceeds
* that limit.
*/
- private static String shortenSubject(String subject) {
+ protected static String shortenSubject(String subject) {
if (subject.length() < 73) {
return subject;
}
return subject.substring(0, 69) + "...";
}
- private Set<String> getEmailsByState(ReviewerStateInternal state) {
+ protected Set<String> getEmailsByState(ReviewerStateInternal state) {
Set<String> reviewers = new TreeSet<>();
try {
for (Account.Id who : changeData.reviewers().byState(state)) {
@@ -698,7 +678,7 @@
return attentionSet;
}
- private boolean getIncludeDiff() {
+ protected boolean getIncludeDiff() {
return args.settings.includeDiff;
}
diff --git a/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java b/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java
index 2ccd0f1..c54c488 100644
--- a/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java
+++ b/java/com/google/gerrit/server/mail/send/CommentChangeEmailDecoratorImpl.java
@@ -68,9 +68,9 @@
/** Send comments, after the author of them hit used Publish Comments in the UI. */
@AutoFactory
public class CommentChangeEmailDecoratorImpl implements CommentChangeEmailDecorator {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ protected static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private class FileCommentGroup {
+ protected class FileCommentGroup {
public String filename;
public int patchSetId;
@@ -123,13 +123,13 @@
}
}
- private EmailArguments args;
- private OutgoingEmail email;
- private ChangeEmail changeEmail;
- private List<? extends Comment> inlineComments = Collections.emptyList();
- @Nullable private String patchSetComment;
- private List<LabelVote> labels = ImmutableList.of();
- private final CommentsUtil commentsUtil;
+ protected EmailArguments args;
+ protected OutgoingEmail email;
+ protected ChangeEmail changeEmail;
+ protected List<? extends Comment> inlineComments = Collections.emptyList();
+ @Nullable protected String patchSetComment;
+ protected List<LabelVote> labels = ImmutableList.of();
+ protected final CommentsUtil commentsUtil;
private final boolean incomingEmailEnabled;
private final String replyToAddress;
private final Supplier<Map<SubmitRequirement, SubmitRequirementResult>>
@@ -241,7 +241,7 @@
}
/** Get the set of accounts whose comments have been replied to in this email. */
- private HashSet<Account.Id> getReplyAccounts() {
+ protected HashSet<Account.Id> getReplyAccounts() {
HashSet<Account.Id> replyAccounts = new HashSet<>();
// Track visited parent UUIDs to avoid cycles.
HashSet<String> visitedUuids = new HashSet<>();
@@ -456,7 +456,7 @@
return commentGroups;
}
- private List<Map<String, Object>> commentBlocksToSoyData(List<CommentFormatter.Block> blocks) {
+ protected List<Map<String, Object>> commentBlocksToSoyData(List<CommentFormatter.Block> blocks) {
return blocks.stream()
.map(
b -> {
@@ -494,7 +494,7 @@
}
@Nullable
- private Repository getRepository() {
+ protected Repository getRepository() {
try {
return args.server.openRepository(changeEmail.getProjectState().getNameKey());
} catch (IOException e) {
@@ -612,7 +612,7 @@
.collect(toImmutableList());
}
- private String getLine(PatchFile fileInfo, short side, int lineNbr) {
+ protected String getLine(PatchFile fileInfo, short side, int lineNbr) {
try {
return fileInfo.getLine(side, lineNbr);
} catch (IOException err) {
@@ -646,7 +646,7 @@
return result.build();
}
- private String getCommentTimestamp() {
+ protected String getCommentTimestamp() {
// Grouping is currently done by timestamp.
return MailProcessingUtil.rfcDateformatter.format(
ZonedDateTime.ofInstant(changeEmail.getTimestamp(), ZoneId.of("UTC")));
diff --git a/java/com/google/gerrit/server/mail/send/DeleteReviewerChangeEmailDecoratorImpl.java b/java/com/google/gerrit/server/mail/send/DeleteReviewerChangeEmailDecoratorImpl.java
index 58ff55f..8953318 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteReviewerChangeEmailDecoratorImpl.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteReviewerChangeEmailDecoratorImpl.java
@@ -27,11 +27,11 @@
/** Let users know that a reviewer and possibly her review have been removed. */
public class DeleteReviewerChangeEmailDecoratorImpl implements DeleteReviewerChangeEmailDecorator {
- private OutgoingEmail email;
- private ChangeEmail changeEmail;
+ protected OutgoingEmail email;
+ protected ChangeEmail changeEmail;
- private final Set<Account.Id> reviewers = new HashSet<>();
- private final Set<Address> reviewersByEmail = new HashSet<>();
+ protected final Set<Account.Id> reviewers = new HashSet<>();
+ protected final Set<Address> reviewersByEmail = new HashSet<>();
@Override
public void addReviewers(Collection<Account.Id> cc) {
@@ -44,7 +44,7 @@
}
@Nullable
- private List<String> getReviewerNames() {
+ protected List<String> getReviewerNames() {
if (reviewers.isEmpty() && reviewersByEmail.isEmpty()) {
return null;
}
diff --git a/java/com/google/gerrit/server/mail/send/DeleteVoteChangeEmailDecorator.java b/java/com/google/gerrit/server/mail/send/DeleteVoteChangeEmailDecorator.java
index 873db91..9949a04 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteVoteChangeEmailDecorator.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteVoteChangeEmailDecorator.java
@@ -20,8 +20,8 @@
/** Send notice about a vote that was removed from a change. */
public class DeleteVoteChangeEmailDecorator implements ChangeEmailDecorator {
- private OutgoingEmail email;
- private ChangeEmail changeEmail;
+ protected OutgoingEmail email;
+ protected ChangeEmail changeEmail;
@Override
public void init(OutgoingEmail email, ChangeEmail changeEmail) {
diff --git a/java/com/google/gerrit/server/mail/send/MergedChangeEmailDecorator.java b/java/com/google/gerrit/server/mail/send/MergedChangeEmailDecorator.java
index cc13ea2..937d7a8 100644
--- a/java/com/google/gerrit/server/mail/send/MergedChangeEmailDecorator.java
+++ b/java/com/google/gerrit/server/mail/send/MergedChangeEmailDecorator.java
@@ -25,6 +25,7 @@
import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.entities.NotifyConfig.NotifyType;
import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.exceptions.EmailException;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -37,13 +38,14 @@
public class MergedChangeEmailDecorator implements ChangeEmailDecorator {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private OutgoingEmail email;
- private ChangeEmail changeEmail;
- private LabelTypes labelTypes;
- private final EmailArguments args;
- private final Optional<String> stickyApprovalDiff;
+ protected OutgoingEmail email;
+ protected ChangeEmail changeEmail;
+ protected LabelTypes labelTypes;
+ protected final EmailArguments args;
+ protected final Optional<String> stickyApprovalDiff;
- MergedChangeEmailDecorator(@Provided EmailArguments args, Optional<String> stickyApprovalDiff) {
+ public MergedChangeEmailDecorator(
+ @Provided EmailArguments args, Optional<String> stickyApprovalDiff) {
this.args = args;
this.stickyApprovalDiff = stickyApprovalDiff;
}
@@ -68,7 +70,7 @@
}
}
- private String getApprovals() {
+ protected String getApprovals() {
try {
Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create();
Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create();
@@ -132,7 +134,7 @@
}
@Override
- public void populateEmailContent() {
+ public void populateEmailContent() throws EmailException {
email.addSoyEmailDataParam("approvals", getApprovals());
if (stickyApprovalDiff.isPresent()) {
email.addSoyEmailDataParam("stickyApprovalDiff", stickyApprovalDiff.get());
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 1eebf32..4dda7f0 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -721,8 +721,9 @@
}
}
+ /** Returns preferred email address for the account. */
@Nullable
- private Address toAddress(Account.Id id) {
+ public Address toAddress(Account.Id id) {
Optional<Account> accountState = args.accountCache.get(id).map(AccountState::account);
if (!accountState.isPresent()) {
return null;
@@ -736,6 +737,11 @@
return Address.create(account.fullName(), e);
}
+ /** Returns the type of notification being sent. */
+ public String getMessageClass() {
+ return messageClass;
+ }
+
/** Set recipients, headers, body of the email. */
public void populateEmailContent() throws EmailException {
for (RecipientType recipientType : notify.accounts().keySet()) {
diff --git a/java/com/google/gerrit/server/mail/send/ReplacePatchSetChangeEmailDecoratorImpl.java b/java/com/google/gerrit/server/mail/send/ReplacePatchSetChangeEmailDecoratorImpl.java
index 4a0fdb4..08b841b 100644
--- a/java/com/google/gerrit/server/mail/send/ReplacePatchSetChangeEmailDecoratorImpl.java
+++ b/java/com/google/gerrit/server/mail/send/ReplacePatchSetChangeEmailDecoratorImpl.java
@@ -48,18 +48,19 @@
implements ReplacePatchSetChangeEmailDecorator {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private final EmailArguments args;
- private OutgoingEmail email;
- private ChangeEmail changeEmail;
- private final Set<Account.Id> reviewers = new HashSet<>();
- private final Set<Account.Id> extraCC = new HashSet<>();
- private final ChangeKind changeKind;
- private final Set<PatchSetApproval> outdatedApprovals = new HashSet<>();
- private final Supplier<Map<SubmitRequirement, SubmitRequirementResult>>
+ protected final EmailArguments args;
+ protected OutgoingEmail email;
+ protected ChangeEmail changeEmail;
+ protected final Set<Account.Id> reviewers = new HashSet<>();
+ protected final Set<Account.Id> extraCC = new HashSet<>();
+ protected final ChangeKind changeKind;
+ protected final Set<PatchSetApproval> outdatedApprovals = new HashSet<>();
+ protected final Supplier<Map<SubmitRequirement, SubmitRequirementResult>>
preUpdateSubmitRequirementResultsSupplier;
- private final Map<SubmitRequirement, SubmitRequirementResult> postUpdateSubmitRequirementResults;
+ protected final Map<SubmitRequirement, SubmitRequirementResult>
+ postUpdateSubmitRequirementResults;
- ReplacePatchSetChangeEmailDecoratorImpl(
+ public ReplacePatchSetChangeEmailDecoratorImpl(
@Provided EmailArguments args,
Project.NameKey project,
Change.Id changeId,
@@ -123,7 +124,7 @@
}
@Nullable
- private ImmutableList<String> getReviewerNames() {
+ protected ImmutableList<String> getReviewerNames() {
List<String> names = new ArrayList<>();
for (Account.Id id : reviewers) {
if (id.equals(email.getFrom())) {
@@ -137,7 +138,7 @@
return names.stream().sorted().collect(toImmutableList());
}
- private ImmutableList<String> formatOutdatedApprovals() {
+ protected ImmutableList<String> formatOutdatedApprovals() {
return outdatedApprovals.stream()
.map(
outdatedApproval ->
diff --git a/java/com/google/gerrit/server/mail/send/RestoredChangeEmailDecorator.java b/java/com/google/gerrit/server/mail/send/RestoredChangeEmailDecorator.java
index 38eab48..fea7ecd 100644
--- a/java/com/google/gerrit/server/mail/send/RestoredChangeEmailDecorator.java
+++ b/java/com/google/gerrit/server/mail/send/RestoredChangeEmailDecorator.java
@@ -20,8 +20,8 @@
/** Send notice about a change being restored by its owner. */
public class RestoredChangeEmailDecorator implements ChangeEmailDecorator {
- private OutgoingEmail email;
- private ChangeEmail changeEmail;
+ protected OutgoingEmail email;
+ protected ChangeEmail changeEmail;
@Override
public void init(OutgoingEmail email, ChangeEmail changeEmail) {
diff --git a/java/com/google/gerrit/server/mail/send/RevertedChangeEmailDecorator.java b/java/com/google/gerrit/server/mail/send/RevertedChangeEmailDecorator.java
index d1cff9c..4328843 100644
--- a/java/com/google/gerrit/server/mail/send/RevertedChangeEmailDecorator.java
+++ b/java/com/google/gerrit/server/mail/send/RevertedChangeEmailDecorator.java
@@ -20,8 +20,8 @@
/** Send notice about a change being reverted. */
public class RevertedChangeEmailDecorator implements ChangeEmailDecorator {
- private OutgoingEmail email;
- private ChangeEmail changeEmail;
+ protected OutgoingEmail email;
+ protected ChangeEmail changeEmail;
@Override
public void init(OutgoingEmail email, ChangeEmail changeEmail) {
diff --git a/java/com/google/gerrit/server/mail/send/StartReviewChangeEmailDecoratorImpl.java b/java/com/google/gerrit/server/mail/send/StartReviewChangeEmailDecoratorImpl.java
index 16fca48..23a6525 100644
--- a/java/com/google/gerrit/server/mail/send/StartReviewChangeEmailDecoratorImpl.java
+++ b/java/com/google/gerrit/server/mail/send/StartReviewChangeEmailDecoratorImpl.java
@@ -27,16 +27,16 @@
/** Sends an email alerting a user to a new change for them to review. */
public class StartReviewChangeEmailDecoratorImpl implements StartReviewChangeEmailDecorator {
- private OutgoingEmail email;
- private ChangeEmail changeEmail;
+ protected OutgoingEmail email;
+ protected ChangeEmail changeEmail;
- private final Set<Account.Id> reviewers = new HashSet<>();
- private final Set<Address> reviewersByEmail = new HashSet<>();
- private final Set<Account.Id> extraCC = new HashSet<>();
- private final Set<Address> extraCCByEmail = new HashSet<>();
- private final Set<Account.Id> removedReviewers = new HashSet<>();
- private final Set<Address> removedByEmailReviewers = new HashSet<>();
- private boolean isCreateChange = false;
+ protected final Set<Account.Id> reviewers = new HashSet<>();
+ protected final Set<Address> reviewersByEmail = new HashSet<>();
+ protected final Set<Account.Id> extraCC = new HashSet<>();
+ protected final Set<Address> extraCCByEmail = new HashSet<>();
+ protected final Set<Account.Id> removedReviewers = new HashSet<>();
+ protected final Set<Address> removedByEmailReviewers = new HashSet<>();
+ protected boolean isCreateChange = false;
@Override
public void addReviewers(Collection<Account.Id> cc) {
@@ -80,7 +80,7 @@
}
@Nullable
- private List<String> getReviewerNames() {
+ protected List<String> getReviewerNames() {
if (reviewers.isEmpty()) {
return null;
}
@@ -92,7 +92,7 @@
}
@Nullable
- private List<String> getRemovedReviewerNames() {
+ protected List<String> getRemovedReviewerNames() {
if (removedReviewers.isEmpty() && removedByEmailReviewers.isEmpty()) {
return null;
}
diff --git a/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java b/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java
index 0289e17..35611eb 100644
--- a/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java
@@ -46,7 +46,7 @@
private final ExecutorService executor;
private final AllUsersName allUsersName;
private final GitRepositoryManager repoManager;
- private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
+ private final ListMultimap<String, ChangeDraftNotesUpdate> draftUpdates;
private PersonIdent serverIdent;
@@ -61,13 +61,13 @@
this.draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
}
- void setDraftUpdates(ListMultimap<String, ChangeDraftUpdate> draftUpdates) {
+ void setDraftUpdates(ListMultimap<String, ChangeDraftNotesUpdate> draftUpdates) {
checkState(isEmpty(), "attempted to set draft comment updates for async execution twice");
boolean allPublishOnly =
- draftUpdates.values().stream().allMatch(ChangeDraftUpdate::canRunAsync);
+ draftUpdates.values().stream().allMatch(ChangeDraftNotesUpdate::canRunAsync);
checkState(allPublishOnly, "not all updates can be run asynchronously");
// Add deep copies to avoid any threading issues.
- for (Map.Entry<String, ChangeDraftUpdate> entry : draftUpdates.entries()) {
+ for (Map.Entry<String, ChangeDraftNotesUpdate> entry : draftUpdates.entries()) {
this.draftUpdates.put(entry.getKey(), entry.getValue().copy());
}
if (draftUpdates.size() > 0) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftNotesUpdate.java
similarity index 82%
rename from java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
rename to java/com/google/gerrit/server/notedb/ChangeDraftNotesUpdate.java
index 0dcf786..8faca67 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftNotesUpdate.java
@@ -27,6 +27,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.ChangeDraftUpdate;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllUsersName;
import com.google.inject.assistedinject.Assisted;
@@ -38,6 +39,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
@@ -54,16 +56,18 @@
*
* <p>This class is not thread safe.
*/
-public class ChangeDraftUpdate extends AbstractChangeUpdate {
- public interface Factory {
- ChangeDraftUpdate create(
+public class ChangeDraftNotesUpdate extends AbstractChangeUpdate implements ChangeDraftUpdate {
+ public interface Factory extends ChangeDraftUpdateFactory {
+ @Override
+ ChangeDraftNotesUpdate create(
ChangeNotes notes,
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
PersonIdent authorIdent,
Instant when);
- ChangeDraftUpdate create(
+ @Override
+ ChangeDraftNotesUpdate create(
Change change,
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
@@ -84,8 +88,8 @@
FIXED
}
- private static Key key(HumanComment c) {
- return new AutoValue_ChangeDraftUpdate_Key(c.getCommitId(), c.key);
+ private static Key key(Comment c) {
+ return new AutoValue_ChangeDraftNotesUpdate_Key(c.getCommitId(), c.key);
}
private final AllUsersName draftsProject;
@@ -95,7 +99,7 @@
@SuppressWarnings("UnusedMethod")
@AssistedInject
- private ChangeDraftUpdate(
+ private ChangeDraftNotesUpdate(
@GerritPersonIdent PersonIdent serverIdent,
AllUsersName allUsers,
ChangeNoteUtil noteUtil,
@@ -109,7 +113,7 @@
}
@AssistedInject
- private ChangeDraftUpdate(
+ private ChangeDraftNotesUpdate(
@GerritPersonIdent PersonIdent serverIdent,
AllUsersName allUsers,
ChangeNoteUtil noteUtil,
@@ -122,44 +126,37 @@
this.draftsProject = allUsers;
}
- public void putComment(HumanComment c) {
+ @Override
+ public void putDraftComment(HumanComment c) {
checkState(!put.contains(c), "comment already added");
verifyComment(c);
put.add(c);
}
- /**
- * Marks a comment for deletion. Called when the comment is deleted because the user published it.
- */
- public void markCommentPublished(HumanComment c) {
+ @Override
+ public void markDraftCommentAsPublished(HumanComment c) {
checkState(!delete.containsKey(key(c)), "comment already marked for deletion");
verifyComment(c);
delete.put(key(c), DeleteReason.PUBLISHED);
}
- /**
- * Marks a comment for deletion. Called when the comment is deleted because the user removed it.
- */
- public void deleteComment(HumanComment c) {
+ @Override
+ public void addDraftCommentForDeletion(HumanComment c) {
checkState(!delete.containsKey(key(c)), "comment already marked for deletion");
verifyComment(c);
delete.put(key(c), DeleteReason.DELETED);
}
- /**
- * Marks a comment for deletion. Called when the comment should have been deleted previously, but
- * wasn't, so we're fixing it up.
- */
- public void deleteComment(ObjectId commitId, Comment.Key key) {
- Key commentKey = new AutoValue_ChangeDraftUpdate_Key(commitId, key);
- checkState(!delete.containsKey(commentKey), "comment already marked for deletion");
- delete.put(commentKey, DeleteReason.FIXED);
+ @Override
+ public void addAllDraftCommentsForDeletion(List<Comment> comments) {
+ comments.forEach(
+ comment -> {
+ Key commentKey = key(comment);
+ checkState(!delete.containsKey(commentKey), "comment already marked for deletion");
+ delete.put(commentKey, DeleteReason.FIXED);
+ });
}
- /**
- * Returns true if all we do in this operations is deletes caused by publishing or fixing up
- * comments.
- */
public boolean canRunAsync() {
return put.isEmpty()
&& delete.values().stream()
@@ -167,15 +164,16 @@
}
/**
- * Returns a copy of the current {@link ChangeDraftUpdate} that contains references to all
- * deletions. Copying of {@link ChangeDraftUpdate} is only allowed if it contains no new comments.
+ * Returns a copy of the current {@link ChangeDraftNotesUpdate} that contains references to all
+ * deletions. Copying of {@link ChangeDraftNotesUpdate} is only allowed if it contains no new
+ * comments.
*/
- ChangeDraftUpdate copy() {
+ ChangeDraftNotesUpdate copy() {
checkState(
put.isEmpty(),
- "copying ChangeDraftUpdate is allowed only if it doesn't contain new comments");
- ChangeDraftUpdate clonedUpdate =
- new ChangeDraftUpdate(
+ "copying ChangeDraftNotesUpdate is allowed only if it doesn't contain new comments");
+ ChangeDraftNotesUpdate clonedUpdate =
+ new ChangeDraftNotesUpdate(
authorIdent,
draftsProject,
noteUtil,
@@ -261,7 +259,7 @@
noteMap = NoteMap.newEmptyMap();
}
// Even though reading from changes might not be enabled, we need to
- // parse any existing revision notes so we can merge them.
+ // parse any existing revision notes, so we can merge them.
return RevisionNoteMap.parse(
noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap, HumanComment.Status.DRAFT);
}
@@ -297,4 +295,15 @@
public boolean isEmpty() {
return delete.isEmpty() && put.isEmpty();
}
+
+ public static Optional<ChangeDraftNotesUpdate> asChangeDraftNotesUpdate(
+ @Nullable ChangeDraftUpdate obj) {
+ if (obj == null) {
+ return Optional.empty();
+ }
+ if (obj instanceof ChangeDraftNotesUpdate) {
+ return Optional.of((ChangeDraftNotesUpdate) obj);
+ }
+ return Optional.empty();
+ }
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 4a31e23..f11e043 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
import static java.util.Comparator.comparing;
@@ -24,14 +25,12 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Multimaps;
import com.google.common.collect.Ordering;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Sets.SetView;
import com.google.common.flogger.FluentLogger;
import com.google.errorprone.annotations.FormatMethod;
import com.google.gerrit.common.Nullable;
@@ -68,6 +67,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
@@ -109,27 +109,18 @@
this.projectCache = projectCache;
}
- @AutoValue
- public abstract static class ScanResult {
- abstract ImmutableSet<Change.Id> fromPatchSetRefs();
-
- abstract ImmutableSet<Change.Id> fromMetaRefs();
-
- public SetView<Change.Id> all() {
- return Sets.union(fromPatchSetRefs(), fromMetaRefs());
- }
- }
-
- public static ScanResult scanChangeIds(Repository repo) throws IOException {
- ImmutableSet.Builder<Change.Id> fromPs = ImmutableSet.builder();
- ImmutableSet.Builder<Change.Id> fromMeta = ImmutableSet.builder();
+ public static ImmutableMap<Change.Id, ObjectId> scanChangeIds(Repository repo)
+ throws IOException {
+ ImmutableMap.Builder<Change.Id, ObjectId> metaIdByChange = ImmutableMap.builder();
for (Ref r : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
- Change.Id id = Change.Id.fromRef(r.getName());
- if (id != null) {
- (r.getName().endsWith(RefNames.META_SUFFIX) ? fromMeta : fromPs).add(id);
+ if (r.getName().endsWith(RefNames.META_SUFFIX)) {
+ Change.Id id = Change.Id.fromRef(r.getName());
+ if (id != null) {
+ metaIdByChange.put(id, r.getObjectId());
+ }
}
}
- return new AutoValue_ChangeNotes_Factory_ScanResult(fromPs.build(), fromMeta.build());
+ return metaIdByChange.build();
}
public ChangeNotes createChecked(Change c) {
@@ -305,27 +296,25 @@
}
public Stream<ChangeNotesResult> scan(
- ScanResult sr, Project.NameKey project, Predicate<Change.Id> changeIdPredicate) {
- Stream<Change.Id> idStream = sr.all().stream();
+ ImmutableMap<Change.Id, ObjectId> metaIdByChange,
+ Project.NameKey project,
+ Predicate<Change.Id> changeIdPredicate) {
+ Stream<Map.Entry<Change.Id, ObjectId>> metaByIdStream = metaIdByChange.entrySet().stream();
if (changeIdPredicate != null) {
- idStream = idStream.filter(changeIdPredicate);
+ metaByIdStream = metaByIdStream.filter(e -> changeIdPredicate.test(e.getKey()));
}
- return idStream.map(id -> scanOneChange(project, sr, id)).filter(Objects::nonNull);
+ return metaByIdStream.map(e -> scanOneChange(project, e)).filter(Objects::nonNull);
}
@Nullable
- private ChangeNotesResult scanOneChange(Project.NameKey project, ScanResult sr, Change.Id id) {
- if (!sr.fromMetaRefs().contains(id)) {
- // Stray patch set refs can happen due to normal error conditions, e.g. failed
- // push processing, so aren't worth even a warning.
- return null;
- }
-
+ private ChangeNotesResult scanOneChange(
+ Project.NameKey project, Map.Entry<Change.Id, ObjectId> metaIdByChangeId) {
+ Change.Id id = metaIdByChangeId.getKey();
// TODO(dborowitz): See discussion in BatchUpdate#newChangeContext.
try {
Change change = ChangeNotes.Factory.newChange(project, id);
logger.atFine().log("adding change %s found in project %s", id, project);
- return toResult(change);
+ return toResult(change, metaIdByChangeId.getValue());
} catch (InvalidServerIdException ise) {
logger.atWarning().withCause(ise).log(
"skipping change %d in project %s because of an invalid server id", id.get(), project);
@@ -334,8 +323,8 @@
}
@Nullable
- private ChangeNotesResult toResult(Change rawChangeFromNoteDb) {
- ChangeNotes n = new ChangeNotes(args, rawChangeFromNoteDb, true, null);
+ private ChangeNotesResult toResult(Change rawChangeFromNoteDb, ObjectId metaId) {
+ ChangeNotes n = new ChangeNotes(args, rawChangeFromNoteDb, true, null, metaId);
try {
n.load();
} catch (Exception e) {
@@ -554,19 +543,18 @@
return Optional.ofNullable(state.mergedOn());
}
- public ImmutableListMultimap<ObjectId, HumanComment> getDraftComments(Account.Id author) {
+ public ImmutableList<HumanComment> getDraftComments(Account.Id author) {
return getDraftComments(author, null);
}
- public ImmutableListMultimap<ObjectId, HumanComment> getDraftComments(
- Account.Id author, @Nullable Ref ref) {
+ ImmutableList<HumanComment> getDraftComments(Account.Id author, @Nullable Ref ref) {
loadDraftComments(author, ref);
// Filter out any zombie draft comments. These are drafts that are also in
// the published map, and arise when the update to All-Users to delete them
// during the publish operation failed.
- return ImmutableListMultimap.copyOf(
- Multimaps.filterEntries(
- draftCommentNotes.getComments(), e -> !getCommentKeys().contains(e.getValue().key)));
+ return draftCommentNotes.getComments().stream()
+ .filter(d -> !getCommentKeys().contains(d.key))
+ .collect(toImmutableList());
}
public ImmutableListMultimap<ObjectId, RobotComment> getRobotComments() {
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 42588cf..23fc000 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -76,10 +76,12 @@
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.server.ChangeDraftUpdate;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
+import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.AttentionSetUtil;
@@ -145,7 +147,7 @@
public static final int MAX_CUSTOM_KEYED_VALUES = 100;
private final NoteDbUpdateManager.Factory updateManagerFactory;
- private final ChangeDraftUpdate.Factory draftUpdateFactory;
+ private final ChangeDraftUpdate.ChangeDraftUpdateFactory draftUpdateFactory;
private final RobotCommentUpdate.Factory robotCommentUpdateFactory;
private final DeleteCommentRewriter.Factory deleteCommentRewriterFactory;
private final ServiceUserClassifier serviceUserClassifier;
@@ -199,7 +201,7 @@
private ChangeUpdate(
@GerritPersonIdent PersonIdent serverIdent,
NoteDbUpdateManager.Factory updateManagerFactory,
- ChangeDraftUpdate.Factory draftUpdateFactory,
+ ChangeDraftUpdate.ChangeDraftUpdateFactory draftUpdateFactory,
RobotCommentUpdate.Factory robotCommentUpdateFactory,
DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
ProjectCache projectCache,
@@ -237,7 +239,7 @@
private ChangeUpdate(
@GerritPersonIdent PersonIdent serverIdent,
NoteDbUpdateManager.Factory updateManagerFactory,
- ChangeDraftUpdate.Factory draftUpdateFactory,
+ ChangeDraftUpdate.ChangeDraftUpdateFactory draftUpdateFactory,
RobotCommentUpdate.Factory robotCommentUpdateFactory,
DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
ServiceUserClassifier serviceUserClassifier,
@@ -388,10 +390,10 @@
verifyComment(c);
createDraftUpdateIfNull();
if (status == HumanComment.Status.DRAFT) {
- draftUpdate.putComment(c);
+ draftUpdate.putDraftComment(c);
} else {
comments.add(c);
- draftUpdate.markCommentPublished(c);
+ draftUpdate.markDraftCommentAsPublished(c);
}
}
@@ -403,7 +405,7 @@
public void deleteComment(HumanComment c) {
verifyComment(c);
- createDraftUpdateIfNull().deleteComment(c);
+ createDraftUpdateIfNull().addDraftCommentForDeletion(c);
}
public void deleteCommentByRewritingHistory(String uuid, String newMessage) {
@@ -445,8 +447,8 @@
}
}
- public void setTopic(String topic) throws ValidationException {
-
+ public void setTopic(String topic, TopicValidator validator) throws ValidationException {
+ validator.validateSize(topic);
if (isIllegalTopic(topic)) {
throw new ValidationException("topic can't contain quotation marks.");
}
@@ -661,29 +663,31 @@
Map<ObjectId, RevisionNoteBuilder> toUpdate) {
// Prohibit various kinds of illegal operations on comments.
Set<Comment.Key> existing = new HashSet<>();
+ List<Comment> draftsToFix = new ArrayList();
for (ChangeRevisionNote rn : existingNotes.values()) {
for (Comment c : rn.getEntities()) {
existing.add(c.key);
- if (draftUpdate != null) {
- // Take advantage of an existing update on All-Users to prune any
- // published comments from drafts. NoteDbUpdateManager takes care of
- // ensuring that this update is applied before its dependent draft
- // update.
- //
- // Deleting aggressively in this way, combined with filtering out
- // duplicate published/draft comments in ChangeNotes#getDraftComments,
- // makes up for the fact that updates between the change repo and
- // All-Users are not atomic.
- //
- // TODO(dborowitz): We might want to distinguish between deleted
- // drafts that we're fixing up after the fact by putting them in a
- // separate commit. But note that we don't care much about the commit
- // graph of the draft ref, particularly because the ref is completely
- // deleted when all drafts are gone.
- draftUpdate.deleteComment(c.getCommitId(), c.key);
- }
+ draftsToFix.add(c);
}
}
+ if (draftUpdate != null) {
+ // Take advantage of an existing update on All-Users to prune any
+ // published comments from drafts. NoteDbUpdateManager takes care of
+ // ensuring that this update is applied before its dependent draft
+ // update.
+ //
+ // Deleting aggressively in this way, combined with filtering out
+ // duplicate published/draft comments in ChangeNotes#getDraftsByChangeAndDraftAuthor,
+ // makes up for the fact that updates between the change repo and
+ // All-Users are not atomic.
+ //
+ // TODO(dborowitz): We might want to distinguish between deleted
+ // drafts that we're fixing up after the fact by putting them in a
+ // separate commit. But note that we don't care much about the commit
+ // graph of the draft ref, particularly because the ref is completely
+ // deleted when all drafts are gone.
+ draftUpdate.addAllDraftCommentsForDeletion(draftsToFix);
+ }
for (RevisionNoteBuilder b : toUpdate.values()) {
for (Comment c : b.put.values()) {
diff --git a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
index 3f3ede1..7d00d2c 100644
--- a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
+++ b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
@@ -19,21 +19,17 @@
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
-import com.google.auto.value.AutoValue;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Sets.SetView;
+import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.HumanComment;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DeleteZombieComments;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -42,15 +38,11 @@
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
-import java.sql.Timestamp;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -58,23 +50,11 @@
import org.eclipse.jgit.transport.ReceiveCommand;
/**
- * This class can be used to clean zombie draft comments refs. More context in <a
- * href="https://gerrit-review.googlesource.com/c/gerrit/+/246233">
- * https://gerrit-review.googlesource.com/c/gerrit/+/246233 </a>
+ * This class can be used to clean zombie draft comments from NoteDB.
*
- * <p>The implementation has two cases for detecting zombie drafts:
- *
- * <ul>
- * <li>An earlier bug in the deletion of draft comments {@code
- * refs/draft-comments/$change_id_short/$change_id/$user_id} caused some draft refs to remain
- * in Git and not get deleted. These refs point to an empty tree. We delete such refs.
- * <li>Inspecting all draft-comment refs. Check for each draft if there exists a published comment
- * with the same UUID. These comments are called zombie drafts. If the program is run in
- * {@link #dryRun} mode, the zombie draft IDs will only be logged for tracking, otherwise they
- * will also be deleted.
- * </uL>
+ * <p>See {@link DeleteZombieComments} for more info.
*/
-public class DeleteZombieCommentsRefs {
+public class DeleteZombieCommentsRefs extends DeleteZombieComments<Ref> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// Number of refs deleted at once in a batch ref-update.
@@ -83,23 +63,12 @@
private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
- private final int cleanupPercentage;
- /**
- * Run the logic in dry run mode only. That is, detected zombie drafts will be logged only but not
- * deleted. Creators of this class can use {@link Factory#create(int, boolean)} to specify the dry
- * run mode. If {@link Factory#create(int)} is used, the dry run mode will be set to its default:
- * true.
- */
- private final boolean dryRun;
-
- private final Consumer<String> uiConsumer;
- @Nullable private final DraftCommentNotes.Factory draftNotesFactory;
- @Nullable private final ChangeNotes.Factory changeNotesFactory;
- @Nullable private final CommentsUtil commentsUtil;
@Nullable private final ChangeUpdate.Factory changeUpdateFactory;
@Nullable private final IdentifiedUser.GenericFactory userFactory;
+ private Repository allUsersRepo;
+
public interface Factory {
DeleteZombieCommentsRefs create(int cleanupPercentage);
@@ -108,22 +77,22 @@
@AssistedInject
public DeleteZombieCommentsRefs(
- AllUsersName allUsers,
+ @Assisted Integer cleanupPercentage,
GitRepositoryManager repoManager,
+ AllUsersName allUsers,
+ DraftCommentsReader draftCommentsReader,
ChangeNotes.Factory changeNotesFactory,
- DraftCommentNotes.Factory draftNotesFactory,
CommentsUtil commentsUtil,
ChangeUpdate.Factory changeUpdateFactory,
- IdentifiedUser.GenericFactory userFactory,
- @Assisted Integer cleanupPercentage) {
+ IdentifiedUser.GenericFactory userFactory) {
this(
- allUsers,
- repoManager,
cleanupPercentage,
/* dryRun= */ true,
(msg) -> {},
+ repoManager,
+ allUsers,
+ draftCommentsReader,
changeNotesFactory,
- draftNotesFactory,
commentsUtil,
changeUpdateFactory,
userFactory);
@@ -131,23 +100,23 @@
@AssistedInject
public DeleteZombieCommentsRefs(
- AllUsersName allUsers,
+ @Assisted Integer cleanupPercentage,
+ @Assisted boolean dryRun,
GitRepositoryManager repoManager,
+ AllUsersName allUsers,
+ DraftCommentsReader draftCommentsReader,
ChangeNotes.Factory changeNotesFactory,
- DraftCommentNotes.Factory draftNotesFactory,
CommentsUtil commentsUtil,
ChangeUpdate.Factory changeUpdateFactory,
- IdentifiedUser.GenericFactory userFactory,
- @Assisted Integer cleanupPercentage,
- @Assisted boolean dryRun) {
+ IdentifiedUser.GenericFactory userFactory) {
this(
- allUsers,
- repoManager,
cleanupPercentage,
dryRun,
(msg) -> {},
+ repoManager,
+ allUsers,
+ draftCommentsReader,
changeNotesFactory,
- draftNotesFactory,
commentsUtil,
changeUpdateFactory,
userFactory);
@@ -159,11 +128,11 @@
Integer cleanupPercentage,
Consumer<String> uiConsumer) {
this(
- allUsers,
- repoManager,
cleanupPercentage,
/* dryRun= */ false,
uiConsumer,
+ repoManager,
+ allUsers,
null,
null,
null,
@@ -172,251 +141,95 @@
}
private DeleteZombieCommentsRefs(
- AllUsersName allUsers,
- GitRepositoryManager repoManager,
Integer cleanupPercentage,
boolean dryRun,
Consumer<String> uiConsumer,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsers,
+ @Nullable DraftCommentsReader draftCommentsReader,
@Nullable ChangeNotes.Factory changeNotesFactory,
- @Nullable DraftCommentNotes.Factory draftNotesFactory,
@Nullable CommentsUtil commentsUtil,
@Nullable ChangeUpdate.Factory changeUpdateFactory,
@Nullable IdentifiedUser.GenericFactory userFactory) {
+ super(
+ cleanupPercentage,
+ dryRun,
+ uiConsumer,
+ repoManager,
+ draftCommentsReader,
+ changeNotesFactory,
+ commentsUtil);
this.allUsers = allUsers;
this.repoManager = repoManager;
- this.cleanupPercentage = (cleanupPercentage == null) ? 100 : cleanupPercentage;
- this.dryRun = dryRun;
- this.uiConsumer = uiConsumer;
- this.draftNotesFactory = draftNotesFactory;
- this.changeNotesFactory = changeNotesFactory;
- this.commentsUtil = commentsUtil;
this.changeUpdateFactory = changeUpdateFactory;
this.userFactory = userFactory;
}
- public void execute() throws IOException {
- deleteDraftRefsThatPointToEmptyTree();
- if (draftNotesFactory != null) {
- deleteDraftCommentsThatAreAlsoPublished();
- }
+ @Override
+ public void setup() throws IOException {
+ allUsersRepo = repoManager.openRepository(allUsers);
}
- private void deleteDraftRefsThatPointToEmptyTree() throws IOException {
- try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
- List<Ref> draftRefs = allUsersRepo.getRefDatabase().getRefsByPrefix(REFS_DRAFT_COMMENTS);
- List<Ref> zombieRefs = filterZombieRefs(allUsersRepo, draftRefs);
+ @Override
+ public void close() throws IOException {
+ allUsersRepo.close();
+ }
- logInfo(
- String.format(
- "Found a total of %d zombie draft refs in %s repo.",
- zombieRefs.size(), allUsers.get()));
+ @Override
+ protected List<Ref> listAllDrafts() throws IOException {
+ return allUsersRepo.getRefDatabase().getRefsByPrefix(REFS_DRAFT_COMMENTS);
+ }
- logInfo(String.format("Cleanup percentage = %d", cleanupPercentage));
- zombieRefs =
- zombieRefs.stream()
- .filter(
- ref -> Change.Id.fromAllUsersRef(ref.getName()).get() % 100 < cleanupPercentage)
- .collect(toImmutableList());
- logInfo(String.format("Number of zombie refs to be cleaned = %d", zombieRefs.size()));
-
- if (dryRun) {
- logInfo(
- "Running in dry run mode. Skipping deletion of draft refs pointing to an empty tree.");
- return;
- }
-
- long zombieRefsCnt = zombieRefs.size();
- long deletedRefsCnt = 0;
- long startTime = System.currentTimeMillis();
-
- for (List<Ref> refsBatch : Iterables.partition(zombieRefs, CHUNK_SIZE)) {
- deleteBatchZombieRefs(allUsersRepo, refsBatch);
- long elapsed = (System.currentTimeMillis() - startTime) / 1000;
- deletedRefsCnt += refsBatch.size();
- logProgress(deletedRefsCnt, zombieRefsCnt, elapsed);
- }
- }
+ @Override
+ protected List<Ref> listEmptyDrafts() throws IOException {
+ List<Ref> zombieRefs = filterZombieRefs(allUsersRepo, listAllDrafts());
+ logInfo(
+ String.format(
+ "Found a total of %d zombie draft refs in %s repo.",
+ zombieRefs.size(), allUsers.get()));
+ return zombieRefs;
}
/**
- * Iterates over all draft refs in All-Users repository. For each draft ref, checks if there
- * exists a published comment with the same UUID and deletes the draft ref if that's the case
- * because it is a zombie draft.
- *
- * @return the number of detected and deleted zombie draft comments.
+ * An earlier bug in the deletion of draft comments {@code
+ * refs/draft-comments/$change_id_short/$change_id/$user_id} caused some draft refs to remain in
+ * Git and not get deleted. These refs point to an empty tree. We delete such refs.
*/
- @VisibleForTesting
- public int deleteDraftCommentsThatAreAlsoPublished() throws IOException {
- try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
- Timestamp earliestZombieTs = null;
- Timestamp latestZombieTs = null;
- int numZombies = 0;
- List<Ref> draftRefs = allUsersRepo.getRefDatabase().getRefsByPrefix(REFS_DRAFT_COMMENTS);
- // Filter the number of draft refs to be processed according to the cleanup percentage.
- draftRefs =
- draftRefs.stream()
- .filter(
- ref -> Change.Id.fromAllUsersRef(ref.getName()).get() % 100 < cleanupPercentage)
- .collect(toImmutableList());
- Set<ChangeUserIDsPair> visitedSet = new HashSet<>();
- ImmutableSet<Change.Id> changeIds =
- draftRefs.stream()
- .map(d -> Change.Id.fromAllUsersRef(d.getName()))
- .collect(ImmutableSet.toImmutableSet());
- Map<Change.Id, Project.NameKey> changeProjectMap = mapChangeIdsToProjects(changeIds);
- for (Ref draftRef : draftRefs) {
- try {
- Change.Id changeId = Change.Id.fromAllUsersRef(draftRef.getName());
- Account.Id accountId = Account.Id.fromRef(draftRef.getName());
- ChangeUserIDsPair changeUserIDsPair = ChangeUserIDsPair.create(changeId, accountId);
- if (!visitedSet.add(changeUserIDsPair)) {
- continue;
- }
- if (!changeProjectMap.containsKey(changeId)) {
- logger.atWarning().log(
- "Could not find a project associated with change ID %s. Skipping draft ref %s.",
- changeId, draftRef.getName());
- continue;
- }
- DraftCommentNotes draftNotes = draftNotesFactory.create(changeId, accountId).load();
- ChangeNotes notes =
- changeNotesFactory.createChecked(changeProjectMap.get(changeId), changeId);
- List<HumanComment> drafts = draftNotes.getComments().values().asList();
- List<HumanComment> published = commentsUtil.publishedHumanCommentsByChange(notes);
- Set<String> publishedIds = toUuid(published);
- List<HumanComment> zombieDrafts =
- drafts.stream()
- .filter(draft -> publishedIds.contains(draft.key.uuid))
- .collect(Collectors.toList());
- for (HumanComment zombieDraft : zombieDrafts) {
- earliestZombieTs = getEarlierTs(earliestZombieTs, zombieDraft.writtenOn);
- latestZombieTs = getLaterTs(latestZombieTs, zombieDraft.writtenOn);
- }
- zombieDrafts.forEach(
- zombieDraft ->
- logger.atWarning().log(
- "Draft comment with uuid '%s' of change %s, account %s, written on %s,"
- + " is a zombie draft that is already published.",
- zombieDraft.key.uuid, changeId, accountId, zombieDraft.writtenOn));
- if (!zombieDrafts.isEmpty() && !dryRun) {
- deleteZombieComments(accountId, notes, zombieDrafts);
- }
- numZombies += zombieDrafts.size();
- } catch (Exception e) {
- logger.atWarning().withCause(e).log("Failed to process ref %s", draftRef.getName());
- }
- }
- if (numZombies > 0) {
- logger.atWarning().log(
- "Detected %d additional zombie drafts (earliest at %s, latest at %s).",
- numZombies, earliestZombieTs, latestZombieTs);
- }
- return numZombies;
+ @Override
+ protected void deleteEmptyDraftsByKey(Collection<Ref> refs) throws IOException {
+ long zombieRefsCnt = refs.size();
+ long deletedRefsCnt = 0;
+ long startTime = System.currentTimeMillis();
+
+ for (List<Ref> refsBatch : Iterables.partition(refs, CHUNK_SIZE)) {
+ deleteZombieDraftsBatch(refsBatch);
+ long elapsed = (System.currentTimeMillis() - startTime) / 1000;
+ deletedRefsCnt += refsBatch.size();
+ logProgress(deletedRefsCnt, zombieRefsCnt, elapsed);
}
}
- @AutoValue
- abstract static class ChangeUserIDsPair {
- abstract Change.Id changeId();
-
- abstract Account.Id accountId();
-
- static ChangeUserIDsPair create(Change.Id changeId, Account.Id accountId) {
- return new AutoValue_DeleteZombieCommentsRefs_ChangeUserIDsPair(changeId, accountId);
+ @Override
+ protected void deleteZombieDrafts(ListMultimap<Ref, HumanComment> drafts) throws IOException {
+ for (Map.Entry<Ref, Collection<HumanComment>> e : drafts.asMap().entrySet()) {
+ deleteZombieDraftsForChange(
+ getAccountId(e.getKey()), getChangeNotes(getChangeId(e.getKey())), e.getValue());
}
}
- /**
- * Accepts a list of draft (zombie) comments for the same change and delete them by executing a
- * {@link ChangeUpdate} on NoteDb. The update is executed using the user account who created this
- * draft.
- */
- private void deleteZombieComments(
- Account.Id accountId, ChangeNotes changeNotes, List<HumanComment> draftsToDelete)
- throws IOException {
- if (changeUpdateFactory == null || userFactory == null) {
- return;
- }
- ChangeUpdate changeUpdate =
- changeUpdateFactory.create(changeNotes, userFactory.create(accountId), TimeUtil.now());
- draftsToDelete.forEach(c -> changeUpdate.deleteComment(c));
- changeUpdate.commit();
- logger.atInfo().log(
- "Deleted zombie draft comments with UUIDs %s",
- draftsToDelete.stream().map(d -> d.key.uuid).collect(Collectors.toList()));
+ @Override
+ protected Change.Id getChangeId(Ref ref) {
+ return Change.Id.fromAllUsersRef(ref.getName());
}
- /**
- * Map each change ID to its associated project.
- *
- * <p>When doing a ref scan of draft refs
- * "refs/draft-comments/$change_id_short/$change_id/$user_id" we don't know which project this
- * draft comment is associated with. The project name is needed to load published comments for the
- * change, hence we map each change ID to its project here by scanning through the change meta ref
- * of the change ID in all projects.
- */
- private Map<Change.Id, Project.NameKey> mapChangeIdsToProjects(
- ImmutableSet<Change.Id> changeIds) {
- Map<Change.Id, Project.NameKey> result = new HashMap<>();
- for (Project.NameKey project : repoManager.list()) {
- try (Repository repo = repoManager.openRepository(project)) {
- SetView<Change.Id> unmappedChangeIds = Sets.difference(changeIds, result.keySet());
- for (Change.Id changeId : unmappedChangeIds) {
- Ref ref = repo.getRefDatabase().exactRef(RefNames.changeMetaRef(changeId));
- if (ref != null) {
- result.put(changeId, project);
- }
- }
- } catch (Exception e) {
- logger.atWarning().withCause(e).log("Failed to open repository for project '%s'.", project);
- }
- if (changeIds.size() == result.size()) {
- // We do not need to scan the remaining repositories
- break;
- }
- }
- if (result.size() != changeIds.size()) {
- logger.atWarning().log(
- "Failed to associate the following change Ids to a project: %s",
- Sets.difference(changeIds, result.keySet()));
- }
- return result;
+ @Override
+ protected Account.Id getAccountId(Ref ref) {
+ return Account.Id.fromRef(ref.getName());
}
- /** Map the list of input comments to their UUIDs. */
- private Set<String> toUuid(List<HumanComment> in) {
- return in.stream().map(c -> c.key.uuid).collect(Collectors.toSet());
- }
-
- private Timestamp getEarlierTs(@Nullable Timestamp t1, Timestamp t2) {
- if (t1 == null) {
- return t2;
- }
- return t1.before(t2) ? t1 : t2;
- }
-
- private Timestamp getLaterTs(@Nullable Timestamp t1, Timestamp t2) {
- if (t1 == null) {
- return t2;
- }
- return t1.after(t2) ? t1 : t2;
- }
-
- private void deleteBatchZombieRefs(Repository allUsersRepo, List<Ref> refsBatch)
- throws IOException {
- try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
- List<ReceiveCommand> deleteCommands =
- refsBatch.stream()
- .map(
- zombieRef ->
- new ReceiveCommand(
- zombieRef.getObjectId(), ObjectId.zeroId(), zombieRef.getName()))
- .collect(toImmutableList());
- BatchRefUpdate bru = allUsersRepo.getRefDatabase().newBatchUpdate();
- bru.setAtomic(true);
- bru.addCommand(deleteCommands);
- RefUpdateUtil.executeChecked(bru, allUsersRepo);
- }
+ @Override
+ protected String loggable(Ref ref) {
+ return ref.getName();
}
private List<Ref> filterZombieRefs(Repository allUsersRepo, List<Ref> allDraftRefs)
@@ -434,15 +247,46 @@
return allUsersRepo.parseCommit(ref.getObjectId()).getTree().getId().equals(EMPTY_TREE_ID);
}
- private void logInfo(String message) {
- logger.atInfo().log("%s", message);
- uiConsumer.accept(message);
- }
-
private void logProgress(long deletedRefsCount, long allRefsCount, long elapsed) {
logInfo(
String.format(
"Deleted %d/%d zombie draft refs (%d seconds)",
deletedRefsCount, allRefsCount, elapsed));
}
+
+ private void deleteZombieDraftsBatch(Collection<Ref> refsBatch) throws IOException {
+ try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+ List<ReceiveCommand> deleteCommands =
+ refsBatch.stream()
+ .map(
+ zombieRef ->
+ new ReceiveCommand(
+ zombieRef.getObjectId(), ObjectId.zeroId(), zombieRef.getName()))
+ .collect(toImmutableList());
+ BatchRefUpdate bru = allUsersRepo.getRefDatabase().newBatchUpdate();
+ bru.setAtomic(true);
+ bru.addCommand(deleteCommands);
+ RefUpdateUtil.executeChecked(bru, allUsersRepo);
+ }
+ }
+
+ /**
+ * Accepts a list of draft (zombie) comments for the same change and delete them by executing a
+ * {@link ChangeUpdate} on NoteDb. The update is executed using the user account who created this
+ * draft.
+ */
+ private void deleteZombieDraftsForChange(
+ Account.Id accountId, ChangeNotes changeNotes, Collection<HumanComment> draftsToDelete)
+ throws IOException {
+ if (changeUpdateFactory == null || userFactory == null) {
+ return;
+ }
+ ChangeUpdate changeUpdate =
+ changeUpdateFactory.create(changeNotes, userFactory.create(accountId), TimeUtil.now());
+ draftsToDelete.forEach(c -> changeUpdate.deleteComment(c));
+ changeUpdate.commit();
+ logger.atInfo().log(
+ "Deleted zombie draft comments with UUIDs %s",
+ draftsToDelete.stream().map(d -> d.key.uuid).collect(toImmutableList()));
+ }
}
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index bdfe378..186b49a 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -19,6 +19,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -78,8 +79,8 @@
return author;
}
- public ImmutableListMultimap<ObjectId, HumanComment> getComments() {
- return comments;
+ public ImmutableList<HumanComment> getComments() {
+ return comments.values().asList();
}
public boolean containsComment(HumanComment c) {
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java b/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java
new file mode 100644
index 0000000..b39c6ef
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/DraftCommentsNotesReader.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2023 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.notedb;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.HumanComment;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+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;
+
+@Singleton
+public class DraftCommentsNotesReader implements DraftCommentsReader {
+ private final DraftCommentNotes.Factory draftCommentNotesFactory;
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsers;
+
+ @Inject
+ DraftCommentsNotesReader(
+ DraftCommentNotes.Factory draftCommentNotesFactory,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsers) {
+ this.draftCommentNotesFactory = draftCommentNotesFactory;
+ this.repoManager = repoManager;
+ this.allUsers = allUsers;
+ }
+
+ @Override
+ public Optional<HumanComment> getDraftComment(
+ ChangeNotes notes, IdentifiedUser author, Comment.Key key) {
+ return getDraftsByChangeAndDraftAuthor(notes, author.getAccountId()).stream()
+ .filter(c -> key.equals(c.key))
+ .findFirst();
+ }
+
+ @Override
+ public List<HumanComment> getDraftsByChangeAndDraftAuthor(ChangeNotes notes, Account.Id author) {
+ return sort(new ArrayList<>(notes.getDraftComments(author)));
+ }
+
+ @Override
+ public List<HumanComment> getDraftsByChangeAndDraftAuthor(Change.Id changeId, Account.Id author) {
+ return sort(
+ new ArrayList<>(draftCommentNotesFactory.create(changeId, author).load().getComments()));
+ }
+
+ @Override
+ public List<HumanComment> getDraftsByPatchSetAndDraftAuthor(
+ ChangeNotes notes, PatchSet.Id psId, Account.Id author) {
+ return sort(
+ notes.load().getDraftComments(author).stream()
+ .filter(c -> c.key.patchSetId == psId.get())
+ .collect(Collectors.toList()));
+ }
+
+ @Override
+ public List<HumanComment> getDraftsByChangeForAllAuthors(ChangeNotes notes) {
+ List<HumanComment> comments = new ArrayList<>();
+ for (Ref ref : getDraftRefs(notes)) {
+ Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+ if (account != null) {
+ comments.addAll(getDraftsByChangeAndDraftAuthor(notes, account));
+ }
+ }
+ return sort(comments);
+ }
+
+ @Override
+ public Set<Account.Id> getUsersWithDrafts(ChangeNotes changeNotes) {
+ Set<Account.Id> res = new HashSet<>();
+ for (Ref ref : getDraftRefs(changeNotes)) {
+ Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+ if (account != null
+ // Double-check that any drafts exist for this user after
+ // filtering out zombies. If some but not all drafts in the ref
+ // were zombies, the returned Ref still includes those zombies;
+ // this is suboptimal, but is ok for the purposes of
+ // draftsByUser(), and easier than trying to rebuild the change at
+ // this point.
+ && !changeNotes.getDraftComments(account, ref).isEmpty()) {
+ res.add(account);
+ }
+ }
+ return res;
+ }
+
+ @Override
+ public Set<Change.Id> getChangesWithDrafts(Account.Id author) {
+ Set<Change.Id> changes = new HashSet<>();
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_DRAFT_COMMENTS)) {
+ Integer accountIdFromRef = RefNames.parseRefSuffix(ref.getName());
+ if (accountIdFromRef != null && accountIdFromRef == author.get()) {
+ Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
+ if (changeId == null) {
+ continue;
+ }
+ changes.add(changeId);
+ }
+ }
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ return changes;
+ }
+
+ private Collection<Ref> getDraftRefs(ChangeNotes notes) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return repo.getRefDatabase()
+ .getRefsByPrefix(RefNames.refsDraftCommentsPrefix(notes.getChangeId()));
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ private List<HumanComment> sort(List<HumanComment> comments) {
+ return CommentsUtil.sort(comments);
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbModule.java b/java/com/google/gerrit/server/notedb/NoteDbModule.java
index d8a5fd5..1024db2 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -17,6 +17,9 @@
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.DraftCommentsReader;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
@@ -37,13 +40,15 @@
@Override
public void configure() {
- factory(ChangeDraftUpdate.Factory.class);
+ factory(ChangeDraftNotesUpdate.Factory.class);
factory(ChangeUpdate.Factory.class);
factory(DeleteCommentRewriter.Factory.class);
factory(DraftCommentNotes.Factory.class);
factory(NoteDbUpdateManager.Factory.class);
factory(RobotCommentNotes.Factory.class);
factory(RobotCommentUpdate.Factory.class);
+ bind(StarredChangesUtil.class).to(StarredChangesUtilNoteDbImpl.class).in(Singleton.class);
+ bind(DraftCommentsReader.class).to(DraftCommentsNotesReader.class).in(Singleton.class);
if (!useTestBindings) {
install(ChangeNotesCache.module());
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 0939ada..5aa5678 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -91,7 +91,7 @@
private final int maxUpdates;
private final int maxPatchSets;
private final ListMultimap<String, ChangeUpdate> changeUpdates;
- private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
+ private final ListMultimap<String, ChangeDraftNotesUpdate> draftUpdates;
private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates;
private final ListMultimap<String, NoteDbRewriter> rewriters;
private final Set<Change.Id> changesToDelete;
@@ -241,9 +241,10 @@
"cannot update & rewrite ref %s in one BatchUpdate",
update.getRefName());
- ChangeDraftUpdate du = update.getDraftUpdate();
- if (du != null) {
- draftUpdates.put(du.getRefName(), du);
+ Optional<ChangeDraftNotesUpdate> du =
+ ChangeDraftNotesUpdate.asChangeDraftNotesUpdate(update.getDraftUpdate());
+ if (du.isPresent()) {
+ draftUpdates.put(du.get().getRefName(), du.get());
}
RobotCommentUpdate rcu = update.getRobotCommentUpdate();
if (rcu != null) {
@@ -281,7 +282,7 @@
changeUpdates.put(update.getRefName(), update);
}
- public void add(ChangeDraftUpdate draftUpdate) {
+ public void add(ChangeDraftNotesUpdate draftUpdate) {
checkNotExecuted();
draftUpdates.put(draftUpdate.getRefName(), draftUpdate);
}
@@ -326,7 +327,7 @@
NonCancellableOperationContext nonCancellableOperationContext =
RequestStateContext.startNonCancellableOperation()) {
stage();
- // ChangeUpdates must execute before ChangeDraftUpdates.
+ // ChangeUpdates must execute before ChangeDraftNotesUpdates.
//
// ChangeUpdate will automatically delete draft comments for any published
// comments, but the updates to the two repos don't happen atomically.
@@ -404,7 +405,8 @@
private void addCommands() throws IOException {
changeRepo.addUpdates(changeUpdates, Optional.of(maxUpdates), Optional.of(maxPatchSets));
if (!draftUpdates.isEmpty()) {
- boolean publishOnly = draftUpdates.values().stream().allMatch(ChangeDraftUpdate::canRunAsync);
+ boolean publishOnly =
+ draftUpdates.values().stream().allMatch(ChangeDraftNotesUpdate::canRunAsync);
if (publishOnly) {
updateAllUsersAsync.setDraftUpdates(draftUpdates);
} else {
diff --git a/java/com/google/gerrit/server/notedb/StarredChangesUtilNoteDbImpl.java b/java/com/google/gerrit/server/notedb/StarredChangesUtilNoteDbImpl.java
new file mode 100644
index 0000000..7dc8f59
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/StarredChangesUtilNoteDbImpl.java
@@ -0,0 +1,332 @@
+// Copyright (C) 2015 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.notedb;
+
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.git.GitUpdateFailureException;
+import com.google.gerrit.git.LockFailureException;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+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.logging.TraceContext.TraceTimer;
+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.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+@Singleton
+public class StarredChangesUtilNoteDbImpl implements StarredChangesUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final String DEFAULT_STAR_LABEL = "star";
+
+ private final GitRepositoryManager repoManager;
+ private final GitReferenceUpdated gitRefUpdated;
+ private final AllUsersName allUsers;
+ private final Provider<PersonIdent> serverIdent;
+
+ @Inject
+ StarredChangesUtilNoteDbImpl(
+ GitRepositoryManager repoManager,
+ GitReferenceUpdated gitRefUpdated,
+ AllUsersName allUsers,
+ @GerritPersonIdent Provider<PersonIdent> serverIdent) {
+ this.repoManager = repoManager;
+ this.gitRefUpdated = gitRefUpdated;
+ this.allUsers = allUsers;
+ this.serverIdent = serverIdent;
+ }
+
+ @Override
+ public boolean isStarred(Account.Id accountId, Change.Id changeId) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return getStarRef(repo, RefNames.refsStarredChanges(changeId, accountId)).isPresent();
+ } catch (IOException e) {
+ throw new StorageException(
+ String.format(
+ "Reading stars from change %d for account %d failed",
+ changeId.get(), accountId.get()),
+ e);
+ }
+ }
+
+ @Override
+ public void star(Account.Id accountId, Change.Id changeId) {
+ updateStar(accountId, changeId, true);
+ }
+
+ @Override
+ public void unstar(Account.Id accountId, Change.Id changeId) {
+ updateStar(accountId, changeId, false);
+ }
+
+ private void updateStar(Account.Id accountId, Change.Id changeId, boolean shouldAdd) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ String refName = RefNames.refsStarredChanges(changeId, accountId);
+ if (shouldAdd) {
+ addRef(repo, refName, null);
+ } else {
+ Optional<Ref> ref = getStarRef(repo, refName);
+ if (ref.isPresent()) {
+ deleteRef(repo, refName, ref.get().getObjectId());
+ }
+ }
+ } catch (IOException e) {
+ throw new StorageException(
+ String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
+ e);
+ }
+ }
+
+ @Override
+ public Set<Change.Id> areStarred(
+ Repository allUsersRepo, List<Change.Id> changeIds, Account.Id caller) {
+ List<String> starRefs =
+ changeIds.stream()
+ .map(c -> RefNames.refsStarredChanges(c, caller))
+ .collect(Collectors.toList());
+ try {
+ return allUsersRepo.getRefDatabase().exactRef(starRefs.toArray(new String[0])).keySet()
+ .stream()
+ .map(r -> Change.Id.fromAllUsersRef(r))
+ .collect(Collectors.toSet());
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log(
+ "Failed getting starred changes for account %d within changes: %s",
+ caller.get(), Joiner.on(", ").join(changeIds));
+ return ImmutableSet.of();
+ }
+ }
+
+ @Override
+ public void unstarAllForChangeDeletion(Change.Id changeId) throws IOException {
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
+ batchUpdate.setAllowNonFastForwards(true);
+ batchUpdate.setRefLogIdent(serverIdent.get());
+ batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
+ for (Account.Id accountId : getStars(repo, changeId)) {
+ String refName = RefNames.refsStarredChanges(changeId, accountId);
+ Ref ref = repo.getRefDatabase().exactRef(refName);
+ if (ref != null) {
+ batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
+ }
+ }
+ batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
+ for (ReceiveCommand command : batchUpdate.getCommands()) {
+ if (command.getResult() != ReceiveCommand.Result.OK) {
+ String message =
+ String.format(
+ "Unstar change %d failed, ref %s could not be deleted: %s",
+ changeId.get(), command.getRefName(), command.getResult());
+ if (command.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
+ throw new LockFailureException(message, batchUpdate);
+ }
+ throw new GitUpdateFailureException(message, batchUpdate);
+ }
+ }
+ }
+ }
+
+ @Override
+ public ImmutableMap<Account.Id, Ref> byChange(Change.Id changeId) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ ImmutableMap.Builder<Account.Id, Ref> builder = ImmutableMap.builder();
+ for (Account.Id accountId : getStars(repo, changeId)) {
+ Optional<Ref> starRef = getStarRef(repo, RefNames.refsStarredChanges(changeId, accountId));
+ if (starRef.isPresent()) {
+ builder.put(accountId, starRef.get());
+ }
+ }
+ return builder.build();
+ } catch (IOException e) {
+ throw new StorageException(
+ String.format("Get accounts that starred change %d failed", changeId.get()), e);
+ }
+ }
+
+ @Override
+ public ImmutableSet<Change.Id> byAccountId(Account.Id accountId) {
+ return byAccountId(accountId, true);
+ }
+
+ @Override
+ public ImmutableSet<Change.Id> byAccountId(Account.Id accountId, boolean skipInvalidChanges) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ ImmutableSet.Builder<Change.Id> builder = ImmutableSet.builder();
+ for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_STARRED_CHANGES)) {
+ Account.Id currentAccountId = Account.Id.fromRef(ref.getName());
+ // Skip all refs that don't correspond with accountId.
+ if (currentAccountId == null || !currentAccountId.equals(accountId)) {
+ continue;
+ }
+
+ // Skip invalid change ids.
+ Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
+ if (skipInvalidChanges && changeId == null) {
+ continue;
+ }
+ builder.add(changeId);
+ }
+ return builder.build();
+ } catch (IOException e) {
+ throw new StorageException(
+ String.format("Get starred changes for account %d failed", accountId.get()), e);
+ }
+ }
+
+ private static Set<Account.Id> getStars(Repository allUsers, Change.Id changeId)
+ throws IOException {
+ String prefix = RefNames.refsStarredChangesPrefix(changeId);
+ RefDatabase refDb = allUsers.getRefDatabase();
+ return refDb.getRefsByPrefix(prefix).stream()
+ .map(r -> r.getName().substring(prefix.length()))
+ .map(refPart -> Ints.tryParse(refPart))
+ .filter(Objects::nonNull)
+ .map(id -> Account.id(id))
+ .collect(toSet());
+ }
+
+ private static Optional<Ref> getStarRef(Repository repo, @Nullable String refName)
+ throws IOException {
+ if (refName == null) {
+ return Optional.empty();
+ }
+ Ref ref = repo.exactRef(refName);
+ return Optional.ofNullable(ref);
+ }
+
+ private static ObjectId writeStarredRefContent(Repository repo) throws IOException {
+ try (ObjectInserter oi = repo.newObjectInserter()) {
+ ObjectId id = oi.insert(Constants.OBJ_BLOB, DEFAULT_STAR_LABEL.getBytes(UTF_8));
+ oi.flush();
+ return id;
+ }
+ }
+
+ private void addRef(Repository repo, String refName, ObjectId oldObjectId) throws IOException {
+ try (TraceTimer traceTimer =
+ TraceContext.newTimer(
+ "Add star ref",
+ Metadata.builder().noteDbRefName(refName).resourceCount(1).build());
+ RevWalk rw = new RevWalk(repo)) {
+ RefUpdate u = repo.updateRef(refName);
+ u.setExpectedOldObjectId(oldObjectId);
+ u.setForceUpdate(true);
+ u.setNewObjectId(writeStarredRefContent(repo));
+ u.setRefLogIdent(serverIdent.get());
+ u.setRefLogMessage("Add star ref", true);
+ try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+ RefUpdate.Result result = u.update(rw);
+ switch (result) {
+ case NEW:
+ case FORCED:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ gitRefUpdated.fire(allUsers, u, null);
+ return;
+ case LOCK_FAILURE:
+ throw new LockFailureException(
+ String.format("Add star ref on ref %s failed", refName), u);
+ case IO_FAILURE:
+ case NOT_ATTEMPTED:
+ case REJECTED:
+ case REJECTED_CURRENT_BRANCH:
+ case RENAMED:
+ case REJECTED_MISSING_OBJECT:
+ case REJECTED_OTHER_REASON:
+ default:
+ throw new StorageException(
+ String.format("Add star ref on ref %s failed: %s", refName, result.name()));
+ }
+ }
+ }
+ }
+
+ private void deleteRef(Repository repo, String refName, ObjectId oldObjectId) throws IOException {
+ if (ObjectId.zeroId().equals(oldObjectId)) {
+ // ref doesn't exist
+ return;
+ }
+
+ try (TraceTimer traceTimer =
+ TraceContext.newTimer(
+ "Delete star ref", Metadata.builder().noteDbRefName(refName).build())) {
+ RefUpdate u = repo.updateRef(refName);
+ u.setForceUpdate(true);
+ u.setExpectedOldObjectId(oldObjectId);
+ u.setRefLogIdent(serverIdent.get());
+ u.setRefLogMessage("Unstar change", true);
+ try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+ RefUpdate.Result result = u.delete();
+ switch (result) {
+ case FORCED:
+ gitRefUpdated.fire(allUsers, u, null);
+ return;
+ case LOCK_FAILURE:
+ throw new LockFailureException(String.format("Delete star ref %s failed", refName), u);
+ case NEW:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ case IO_FAILURE:
+ case NOT_ATTEMPTED:
+ case REJECTED:
+ case REJECTED_CURRENT_BRANCH:
+ case RENAMED:
+ case REJECTED_MISSING_OBJECT:
+ case REJECTED_OTHER_REASON:
+ default:
+ throw new StorageException(
+ String.format("Delete star ref %s failed: %s", refName, result.name()));
+ }
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index 0818f23..e27faf6 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -83,6 +83,10 @@
return cfg.getBoolean("change", null, "cacheAutomerge", true);
}
+ public static boolean diff3ConflictView(Config cfg) {
+ return cfg.getBoolean("change", null, "diff3ConflictView", false);
+ }
+
private enum OperationType {
CACHE_LOAD,
IN_MEMORY_WRITE,
@@ -93,6 +97,7 @@
private final Timer1<OperationType> latency;
private final Provider<PersonIdent> gerritIdentProvider;
private final boolean save;
+ private final boolean useDiff3;
private final ThreeWayMergeStrategy configuredMergeStrategy;
@Inject
@@ -117,6 +122,7 @@
.setUnit("milliseconds"),
operationTypeField);
this.save = cacheAutomerge(cfg);
+ this.useDiff3 = diff3ConflictView(cfg);
this.gerritIdentProvider = gerritIdentProvider;
this.configuredMergeStrategy = MergeUtil.getMergeStrategy(cfg);
}
@@ -254,7 +260,8 @@
merge.getParent(0),
"BRANCH",
merge.getParent(1),
- m.getMergeResults());
+ m.getMergeResults(),
+ useDiff3);
}
logger.atFine().log("AutoMerge treeId=%s", treeId.name());
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index ba292e6..a3a041b 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -445,12 +445,15 @@
if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) {
String logMessage =
String.format(
- "'%s' can perform '%s' with force=%s on project '%s' for ref '%s'",
+ "'%s' can perform '%s' with force=%s on project '%s' for ref '%s'"
+ + " (allowed for group '%s' by rule '%s')",
getUser().getLoggableName(),
permissionName,
withForce,
projectControl.getProject().getName(),
- refName);
+ refName,
+ pr.getGroup().getUUID().get(),
+ pr);
LoggingContext.getInstance().addAclLogRecord(logMessage);
logger.atFine().log("%s (caller: %s)", logMessage, callerFinder.findCallerLazy());
}
diff --git a/java/com/google/gerrit/server/plugins/PluginCleanerTask.java b/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
index 6919bbc..5925148 100644
--- a/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
+++ b/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
@@ -41,15 +41,6 @@
@Override
public void run() {
- try {
- for (int t = 0; t < 2 * (attempts + 1); t++) {
- System.gc();
- Thread.sleep(50);
- }
- } catch (InterruptedException e) {
- // Ignored
- }
-
int left = loader.processPendingCleanups();
synchronized (this) {
pending = left;
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 3263636..38b2dda 100644
--- a/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -350,7 +350,6 @@
disabled.clear();
broken.clear();
if (!toCleanup.isEmpty()) {
- System.gc();
processPendingCleanups();
}
}
diff --git a/java/com/google/gerrit/server/plugins/PluginRestApiModule.java b/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
index 8dbea78..b47db0d 100644
--- a/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
+++ b/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
@@ -27,12 +27,13 @@
requireBinding(Key.get(PluginUser.Factory.class));
bind(PluginsCollection.class);
DynamicMap.mapOf(binder(), PLUGIN_KIND);
+
create(PLUGIN_KIND).to(InstallPlugin.Create.class);
put(PLUGIN_KIND).to(InstallPlugin.Overwrite.class);
delete(PLUGIN_KIND).to(DisablePlugin.class);
- get(PLUGIN_KIND, "status").to(GetStatus.class);
post(PLUGIN_KIND, "disable").to(DisablePlugin.class);
post(PLUGIN_KIND, "enable").to(EnablePlugin.class);
post(PLUGIN_KIND, "reload").to(ReloadPlugin.class);
+ get(PLUGIN_KIND, "status").to(GetStatus.class);
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
index 9463b39..4946bea 100644
--- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -35,7 +35,6 @@
import com.google.gerrit.extensions.api.projects.CheckProjectResultInfo.AutoCloseableChangesCheckResult;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -44,7 +43,6 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.ChangeJson;
-import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.change.ChangeData;
@@ -74,7 +72,7 @@
private final RetryHelper retryHelper;
private final ChangeJson.Factory changeJsonFactory;
private final IndexConfig indexConfig;
- private final DynamicItem<UrlFormatter> urlFormatter;
+ private final ChangeUtil changeUtil;
@Inject
ProjectsConsistencyChecker(
@@ -82,12 +80,12 @@
RetryHelper retryHelper,
ChangeJson.Factory changeJsonFactory,
IndexConfig indexConfig,
- DynamicItem<UrlFormatter> urlFormatter) {
+ ChangeUtil changeUtil) {
this.repoManager = repoManager;
this.retryHelper = retryHelper;
this.changeJsonFactory = changeJsonFactory;
this.indexConfig = indexConfig;
- this.urlFormatter = urlFormatter;
+ this.changeUtil = changeUtil;
}
public CheckProjectResultInfo check(Project.NameKey projectName, CheckProjectInput input)
@@ -174,7 +172,7 @@
mergedSha1s.add(commitId);
// Consider all Change-Id lines since this is what ReceiveCommits#autoCloseChanges does.
- List<String> changeIds = ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter.get());
+ List<String> changeIds = changeUtil.getChangeIdsFromFooter(commit);
// Number of predicates that we need to add for this commit, 1 per Change-Id plus one for
// the commit.
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 854897f6..d2e139f 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -28,7 +28,6 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
-import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
@@ -63,12 +62,12 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.StarRef;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.CommentThread;
import com.google.gerrit.server.change.CommentThreads;
@@ -102,7 +101,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -317,6 +316,7 @@
null,
null,
null,
+ null,
serverId,
virtualIdAlgo,
project,
@@ -341,6 +341,8 @@
private final ChangeMessagesUtil cmUtil;
private final ChangeNotes.Factory notesFactory;
private final CommentsUtil commentsUtil;
+
+ private final DraftCommentsReader draftCommentsReader;
private final GitRepositoryManager repoManager;
private final MergeUtilFactory mergeUtilFactory;
private final MergeabilityCache mergeabilityCache;
@@ -395,11 +397,11 @@
* Map from {@link com.google.gerrit.entities.Account.Id} to the tip of the draft comments ref for
* this change and the user.
*/
- private Map<Account.Id, ObjectId> draftsByUser;
+ private Set<Account.Id> usersWithDrafts;
- private ImmutableListMultimap<Account.Id, String> stars;
- private StarsOf starsOf;
- private ImmutableMap<Account.Id, StarRef> starRefs;
+ private ImmutableList<Account.Id> stars;
+ private Account.Id starredBy;
+ private ImmutableMap<Account.Id, Ref> starRefs;
private ReviewerSet reviewers;
private ReviewerByEmailSet reviewersByEmail;
private ReviewerSet pendingReviewers;
@@ -427,6 +429,7 @@
ChangeMessagesUtil cmUtil,
ChangeNotes.Factory notesFactory,
CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
GitRepositoryManager repoManager,
MergeUtilFactory mergeUtilFactory,
MergeabilityCache mergeabilityCache,
@@ -440,7 +443,7 @@
SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
@GerritServerId String gerritServerId,
ChangeNumberVirtualIdAlgorithm virtualIdFunc,
- @Assisted Project.NameKey project,
+ @Assisted NameKey project,
@Assisted Change.Id id,
@Assisted @Nullable Change change,
@Assisted @Nullable ChangeNotes notes) {
@@ -449,6 +452,7 @@
this.cmUtil = cmUtil;
this.notesFactory = notesFactory;
this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.repoManager = repoManager;
this.mergeUtilFactory = mergeUtilFactory;
this.mergeabilityCache = mergeabilityCache;
@@ -1202,7 +1206,17 @@
}
public Set<Account.Id> draftsByUser() {
- return draftRefs().keySet();
+ if (usersWithDrafts == null) {
+ if (!lazyload()) {
+ return Collections.emptySet();
+ }
+ Change c = change();
+ if (c == null) {
+ return Collections.emptySet();
+ }
+ usersWithDrafts = draftCommentsReader.getUsersWithDrafts(notes());
+ }
+ return usersWithDrafts;
}
public boolean isReviewedBy(Account.Id accountId) {
@@ -1265,25 +1279,25 @@
return customKeyedValues;
}
- public ImmutableListMultimap<Account.Id, String> stars() {
+ public void setCustomKeyedValues(Map<String, String> customKeyedValues) {
+ this.customKeyedValues = ImmutableMap.copyOf(customKeyedValues);
+ }
+
+ public ImmutableList<Account.Id> stars() {
if (stars == null) {
if (!lazyload()) {
- return ImmutableListMultimap.of();
+ return ImmutableList.of();
}
- ImmutableListMultimap.Builder<Account.Id, String> b = ImmutableListMultimap.builder();
- for (Map.Entry<Account.Id, StarRef> e : starRefs().entrySet()) {
- b.putAll(e.getKey(), e.getValue().labels());
- }
- return b.build();
+ return starRefs().keySet().asList();
}
return stars;
}
- public void setStars(ListMultimap<Account.Id, String> stars) {
- this.stars = ImmutableListMultimap.copyOf(stars);
+ public void setStars(List<Account.Id> accountIds) {
+ this.stars = ImmutableList.copyOf(accountIds);
}
- private ImmutableMap<Account.Id, StarRef> starRefs() {
+ private ImmutableMap<Account.Id, Ref> starRefs() {
if (starRefs == null) {
if (!lazyload()) {
return ImmutableMap.of();
@@ -1293,23 +1307,25 @@
return starRefs;
}
- public Set<String> stars(Account.Id accountId) {
- if (starsOf != null) {
- if (!starsOf.accountId().equals(accountId)) {
- starsOf = null;
+ public boolean isStarred(Account.Id accountId) {
+ if (starredBy != null) {
+ if (!starredBy.equals(accountId)) {
+ starredBy = null;
}
}
- if (starsOf == null) {
- if (stars != null) {
- starsOf = StarsOf.create(accountId, stars.get(accountId));
+ if (starredBy == null) {
+ if (stars != null && stars.contains(accountId)) {
+ starredBy = accountId;
} else {
if (!lazyload()) {
- return ImmutableSet.of();
+ return false;
}
- starsOf = StarsOf.create(accountId, starredChangesUtil.getLabels(accountId, legacyId));
+ if (starredChangesUtil.isStarred(accountId, legacyId)) {
+ starredBy = accountId;
+ }
}
}
- return starsOf.stars();
+ return starredBy != null;
}
/**
@@ -1380,16 +1396,16 @@
public void setRefStates(ImmutableSetMultimap<Project.NameKey, RefState> refStates) {
this.refStates = refStates;
- if (draftsByUser == null) {
- // Recover draft refs as well. Draft comments are represented as refs in the repository.
+ if (usersWithDrafts == null) {
+ // Recover draft state as well.
// ChangeData exposes #draftsByUser which just provides a Set of Account.Ids of users who
// have drafts comments on this change. Recovering this list from RefStates makes it
// available even on ChangeData instances retrieved from the index.
- draftsByUser = new HashMap<>();
+ usersWithDrafts = new HashSet<>();
if (refStates.containsKey(allUsersName)) {
refStates.get(allUsersName).stream()
.filter(r -> RefNames.isRefsDraftsComments(r.ref()))
- .forEach(r -> draftsByUser.put(Account.Id.fromRef(r.ref()), r.id()));
+ .forEach(r -> usersWithDrafts.add(Account.Id.fromRef(r.ref())));
}
}
}
@@ -1412,43 +1428,4 @@
public abstract Instant ts();
}
-
- @AutoValue
- abstract static class StarsOf {
- private static StarsOf create(Account.Id accountId, Iterable<String> stars) {
- return new AutoValue_ChangeData_StarsOf(accountId, ImmutableSortedSet.copyOf(stars));
- }
-
- public abstract Account.Id accountId();
-
- public abstract ImmutableSortedSet<String> stars();
- }
-
- private Map<Account.Id, ObjectId> draftRefs() {
- if (draftsByUser == null) {
- if (!lazyload()) {
- return Collections.emptyMap();
- }
- Change c = change();
- if (c == null) {
- return Collections.emptyMap();
- }
-
- draftsByUser = new HashMap<>();
- for (Ref ref : commentsUtil.getDraftRefs(notes().getChangeId())) {
- Account.Id account = Account.Id.fromRefSuffix(ref.getName());
- if (account != null
- // Double-check that any drafts exist for this user after
- // filtering out zombies. If some but not all drafts in the ref
- // were zombies, the returned Ref still includes those zombies;
- // this is suboptimal, but is ok for the purposes of
- // draftsByUser(), and easier than trying to rebuild the change at
- // this point.
- && !notes().getDraftComments(account, ref).isEmpty()) {
- draftsByUser.put(account, ref.getObjectId());
- }
- }
- }
- return draftsByUser;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 528d0ce..6d4d74d 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -23,7 +23,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.change.HashtagsUtil;
import com.google.gerrit.server.index.change.ChangeField;
@@ -73,9 +73,10 @@
* Returns a predicate that matches changes where the provided {@link
* com.google.gerrit.entities.Account.Id} has a pending draft comment.
*/
- public static Predicate<ChangeData> draftBy(CommentsUtil commentsUtil, Account.Id id) {
+ public static Predicate<ChangeData> draftBy(
+ DraftCommentsReader draftCommentsReader, Account.Id id) {
Set<Predicate<ChangeData>> changeIdPredicates =
- commentsUtil.getChangesWithDrafts(id).stream()
+ draftCommentsReader.getChangesWithDrafts(id).stream()
.map(ChangePredicates::idStr)
.collect(toImmutableSet());
return changeIdPredicates.isEmpty()
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 57b59ef..b3fa087 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -36,6 +36,7 @@
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
@@ -55,6 +56,7 @@
import com.google.gerrit.index.query.QueryRequiresAuthException;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.account.AccountCache;
@@ -165,6 +167,7 @@
public static final String FIELD_COMMENTBY = "commentby";
public static final String FIELD_COMMIT = "commit";
public static final String FIELD_COMMITTER = "committer";
+ public static final String FIELD_CUSTOM_KEYED_VALUES = "custom_keyed_values";
public static final String FIELD_DIRECTORY = "directory";
public static final String FIELD_EXACTCOMMITTER = "exactcommitter";
public static final String FIELD_EXTENSION = "extension";
@@ -255,6 +258,7 @@
final ChangeIndex index;
final ChangeIndexRewriter rewriter;
final CommentsUtil commentsUtil;
+ final DraftCommentsReader draftCommentsReader;
final ConflictsCache conflictsCache;
final DynamicMap<ChangeHasOperandFactory> hasOperands;
final DynamicMap<ChangeIsOperandFactory> isOperands;
@@ -293,6 +297,7 @@
PermissionBackend permissionBackend,
ChangeData.Factory changeDataFactory,
CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
AccountResolver accountResolver,
GroupBackend groupBackend,
AllProjectsName allProjectsName,
@@ -325,6 +330,7 @@
permissionBackend,
changeDataFactory,
commentsUtil,
+ draftCommentsReader,
accountResolver,
groupBackend,
allProjectsName,
@@ -360,6 +366,7 @@
PermissionBackend permissionBackend,
ChangeData.Factory changeDataFactory,
CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
AccountResolver accountResolver,
GroupBackend groupBackend,
AllProjectsName allProjectsName,
@@ -390,6 +397,7 @@
this.permissionBackend = permissionBackend;
this.changeDataFactory = changeDataFactory;
this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.accountResolver = accountResolver;
this.groupBackend = groupBackend;
this.allProjectsName = allProjectsName;
@@ -428,6 +436,7 @@
permissionBackend,
changeDataFactory,
commentsUtil,
+ draftCommentsReader,
accountResolver,
groupBackend,
allProjectsName,
@@ -492,8 +501,8 @@
protected final Arguments args;
protected Map<String, String> hasOperandAliases = Collections.emptyMap();
- private final Map<Account.Id, DestinationList> destinationListByAccount = new HashMap<>();
- private final Map<Account.Id, QueryList> queryListByAccount = new HashMap<>();
+ private final Map<BranchNameKey, DestinationList> destinationListByBranch = new HashMap<>();
+ private final Map<BranchNameKey, QueryList> queryListByBranch = new HashMap<>();
private static final Splitter RULE_SPLITTER = Splitter.on("=");
private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
@@ -1176,7 +1185,7 @@
}
private Predicate<ChangeData> draftBySelf() throws QueryParseException {
- return ChangePredicates.draftBy(args.commentsUtil, self());
+ return ChangePredicates.draftBy(args.draftCommentsReader, self());
}
@Operator
@@ -1412,11 +1421,16 @@
@Operator
public Predicate<ChangeData> query(String value) throws QueryParseException {
- // [name=]<name>[,user=<user>] || [user=<user>,][name=]<name>
+ // [name=]NAME[,user=USER|,group=GROUP]
PredicateArgs inputArgs = new PredicateArgs(value);
String name = null;
Account.Id account = null;
+ GroupDescription.Internal group = null;
+ if (inputArgs.keyValue.containsKey(ARG_ID_USER)
+ && inputArgs.keyValue.containsKey(ARG_ID_GROUP)) {
+ throw new QueryParseException("User and group arguments are mutually exclusive");
+ }
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
name = inputArgs.keyValue.get(ARG_ID_NAME).value();
@@ -1440,7 +1454,23 @@
account = self();
}
- String query = getQueryList(account).getQuery(name);
+ // [,group=<group>]
+ if (inputArgs.keyValue.containsKey(ARG_ID_GROUP)) {
+ AccountGroup.UUID groupId =
+ parseGroup(inputArgs.keyValue.get(ARG_ID_GROUP).value()).getUUID();
+ GroupDescription.Basic backendGroup = args.groupBackend.get(groupId);
+ if (!(backendGroup instanceof GroupDescription.Internal)) {
+ throw error(backendGroup.getName() + " is not an Internal group");
+ }
+ group = (GroupDescription.Internal) backendGroup;
+ }
+
+ BranchNameKey branch = BranchNameKey.create(args.allUsersName, RefNames.refsUsers(account));
+ if (group != null) {
+ branch = BranchNameKey.create(args.allUsersName, RefNames.refsGroups(group.getGroupUUID()));
+ }
+
+ String query = getQueryList(branch).getQuery(name);
if (query != null) {
return parse(query);
}
@@ -1453,17 +1483,19 @@
throw new QueryParseException("Unknown named query: " + name);
}
- protected QueryList getQueryList(Account.Id account) throws ConfigInvalidException, IOException {
- QueryList ql = queryListByAccount.get(account);
+ protected QueryList getQueryList(BranchNameKey branch)
+ throws ConfigInvalidException, IOException {
+ QueryList ql = queryListByBranch.get(branch);
if (ql == null) {
- ql = loadQueryList(account);
- queryListByAccount.put(account, ql);
+ ql = loadQueryList(branch);
+ queryListByBranch.put(branch, ql);
}
return ql;
}
- protected QueryList loadQueryList(Account.Id account) throws ConfigInvalidException, IOException {
- VersionedAccountQueries q = VersionedAccountQueries.forUser(account);
+ protected QueryList loadQueryList(BranchNameKey branch)
+ throws ConfigInvalidException, IOException {
+ VersionedAccountQueries q = VersionedAccountQueries.forBranch(branch);
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
q.load(args.allUsersName, git);
}
@@ -1478,11 +1510,16 @@
@Operator
public Predicate<ChangeData> destination(String value) throws QueryParseException {
- // [name=]<name>[,user=<user>] || [user=<user>,][name=]<name>
+ // [name=]<name>[,user=<user>|,group=<group>] || [group=<group>,|user=<user>,][name=]<name>
PredicateArgs inputArgs = new PredicateArgs(value);
String name = null;
Account.Id account = null;
+ GroupDescription.Internal group = null;
+ if (inputArgs.keyValue.containsKey(ARG_ID_USER)
+ && inputArgs.keyValue.containsKey(ARG_ID_GROUP)) {
+ throw new QueryParseException("User and group arguments are mutually exclusive");
+ }
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
name = inputArgs.keyValue.get(ARG_ID_NAME).value();
@@ -1506,7 +1543,23 @@
account = self();
}
- Set<BranchNameKey> destinations = getDestinationList(account).getDestinations(name);
+ // [,group=<group>]
+ if (inputArgs.keyValue.containsKey(ARG_ID_GROUP)) {
+ AccountGroup.UUID groupId =
+ parseGroup(inputArgs.keyValue.get(ARG_ID_GROUP).value()).getUUID();
+ GroupDescription.Basic backendGroup = args.groupBackend.get(groupId);
+ if (!(backendGroup instanceof GroupDescription.Internal)) {
+ throw error(backendGroup.getName() + " is not an Internal group");
+ }
+ group = (GroupDescription.Internal) backendGroup;
+ }
+
+ BranchNameKey branch = BranchNameKey.create(args.allUsersName, RefNames.refsUsers(account));
+ if (group != null) {
+ branch = BranchNameKey.create(args.allUsersName, RefNames.refsGroups(group.getGroupUUID()));
+ }
+ Set<BranchNameKey> destinations = getDestinationList(branch).getDestinations(name);
+
if (destinations != null && !destinations.isEmpty()) {
return new BranchSetIndexPredicate(FIELD_DESTINATION + ":" + value, destinations);
}
@@ -1519,19 +1572,19 @@
throw new QueryParseException("Unknown named destination: " + name);
}
- protected DestinationList getDestinationList(Account.Id account)
+ protected DestinationList getDestinationList(BranchNameKey branch)
throws ConfigInvalidException, RepositoryNotFoundException, IOException {
- DestinationList dl = destinationListByAccount.get(account);
+ DestinationList dl = destinationListByBranch.get(branch);
if (dl == null) {
- dl = loadDestinationList(account);
- destinationListByAccount.put(account, dl);
+ dl = loadDestinationList(branch);
+ destinationListByBranch.put(branch, dl);
}
return dl;
}
- protected DestinationList loadDestinationList(Account.Id account)
+ protected DestinationList loadDestinationList(BranchNameKey branch)
throws ConfigInvalidException, RepositoryNotFoundException, IOException {
- VersionedAccountDestinations d = VersionedAccountDestinations.forUser(account);
+ VersionedAccountDestinations d = VersionedAccountDestinations.forBranch(branch);
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
d.load(args.allUsersName, git);
}
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java
index ffd4497..0d6dc3c 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicates.java
@@ -74,7 +74,7 @@
LabelPredicate.Args args,
String label,
int expVal,
- Account.Id account,
+ @Nullable Account.Id account,
@Nullable Integer count) {
super(ChangeField.LABEL_SPEC, ChangeField.formatLabel(label, expVal, account, count));
this.matcher = new Matcher(args, label, expVal, account, count);
@@ -108,7 +108,7 @@
@Nullable protected final Integer count;
/** Account ID that has voted on the label. */
- protected final Account.Id account;
+ @Nullable protected final Account.Id account;
protected final AccountGroup.UUID group;
@@ -120,7 +120,7 @@
LabelPredicate.Args args,
String label,
int expVal,
- Account.Id account,
+ @Nullable Account.Id account,
@Nullable Integer count) {
this.permissionBackend = args.permissionBackend;
this.accountResolver = args.accountResolver;
@@ -246,9 +246,10 @@
}
private boolean isMagicUser() {
- return account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)
- || account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
- || account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID);
+ return account != null
+ && (account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)
+ || account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
+ || account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID));
}
}
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 76ee627..3e471fb 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -35,6 +35,7 @@
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.query.InternalQuery;
import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
@@ -118,6 +119,11 @@
return query(or(preds));
}
+ @UsedAt(UsedAt.Project.GOOGLE)
+ public List<ChangeData> byCustomKeyedValue(String keyValue) {
+ return query(new ChangeIndexPredicate(ChangeField.CUSTOM_KEYED_VALUES_SPEC, keyValue));
+ }
+
public List<ChangeData> byBranchKey(BranchNameKey branch, Change.Key key) {
return query(byBranchKeyPred(branch, key));
}
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java
index 9ee4852..420ab61d 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicates.java
@@ -80,7 +80,7 @@
public IndexMatcher(
LabelPredicate.Args args,
MagicLabelVote magicLabelVote,
- Account.Id account,
+ @Nullable Account.Id account,
@Nullable Integer count) {
super(args, magicLabelVote, account, count);
}
@@ -102,7 +102,7 @@
public IndexMagicLabelPredicate(
LabelPredicate.Args args,
MagicLabelVote magicLabelVote,
- Account.Id account,
+ @Nullable Account.Id account,
@Nullable Integer count) {
super(
ChangeField.LABEL_SPEC,
@@ -128,7 +128,7 @@
private abstract static class Matcher {
protected final LabelPredicate.Args args;
protected final MagicLabelVote magicLabelVote;
- protected final Account.Id account;
+ @Nullable protected final Account.Id account;
@Nullable protected final Integer count;
public Matcher(
@@ -139,7 +139,7 @@
public Matcher(
LabelPredicate.Args args,
MagicLabelVote magicLabelVote,
- Account.Id account,
+ @Nullable Account.Id account,
@Nullable Integer count) {
this.account = account;
this.args = args;
@@ -180,9 +180,12 @@
}
public boolean ignoresUploaderApprovals() {
- logger.atFine().log("account = %d", account.get());
- return account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
- || account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID);
+ logger.atFine().log("account = %s", account);
+ if (account != null) {
+ return account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
+ || account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID);
+ }
+ return false;
}
private boolean matchAny(ChangeData changeData, LabelType labelType) {
diff --git a/java/com/google/gerrit/server/query/project/ProjectPredicates.java b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
index a7b0743..f2ef497 100644
--- a/java/com/google/gerrit/server/query/project/ProjectPredicates.java
+++ b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
@@ -28,10 +28,18 @@
return new ProjectPredicate(ProjectField.NAME_SPEC, nameKey.get());
}
+ public static Predicate<ProjectData> prefix(String prefix) {
+ return new ProjectPredicate(ProjectField.PREFIX_NAME_SPEC, prefix);
+ }
+
public static Predicate<ProjectData> parent(Project.NameKey parentNameKey) {
return new ProjectPredicate(ProjectField.PARENT_NAME_SPEC, parentNameKey.get());
}
+ public static Predicate<ProjectData> parent2(Project.NameKey parentNameKey) {
+ return new ProjectPredicate(ProjectField.PARENT_NAME_2_SPEC, parentNameKey.get());
+ }
+
public static Predicate<ProjectData> inname(String name) {
return new ProjectPredicate(ProjectField.NAME_PART_SPEC, name.toLowerCase(Locale.US));
}
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index 616468e..ade6606 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -26,6 +26,7 @@
*/
public interface ProjectQueryBuilder {
String FIELD_LIMIT = "limit";
+ String FIELD_SUBSTRING = "substring";
/** See {@link com.google.gerrit.index.query.QueryBuilder#parse(String)}. */
Predicate<ProjectData> parse(String query) throws QueryParseException;
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
index 599683e..b15bc6b 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
@@ -19,7 +19,12 @@
import com.google.common.primitives.Ints;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.project.ProjectData;
+import com.google.gerrit.index.project.ProjectField;
+import com.google.gerrit.index.project.ProjectIndex;
+import com.google.gerrit.index.project.ProjectIndexCollection;
+import com.google.gerrit.index.project.ProjectSubstringPredicate;
import com.google.gerrit.index.query.LimitPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
@@ -34,9 +39,12 @@
private static final QueryBuilder.Definition<ProjectData, ProjectQueryBuilderImpl> mydef =
new QueryBuilder.Definition<>(ProjectQueryBuilderImpl.class);
+ private final ProjectIndex index;
+
@Inject
- ProjectQueryBuilderImpl() {
+ ProjectQueryBuilderImpl(ProjectIndexCollection indexes) {
super(mydef, null);
+ this.index = indexes.getSearchIndex();
}
@Operator
@@ -45,8 +53,22 @@
}
@Operator
+ public Predicate<ProjectData> prefix(String prefix) throws QueryParseException {
+ checkOperatorAvailable(ProjectField.PREFIX_NAME_SPEC, "prefix");
+ return ProjectPredicates.prefix(prefix);
+ }
+
+ @Operator
+ public Predicate<ProjectData> substring(String substring) {
+ return new ProjectSubstringPredicate(ProjectQueryBuilder.FIELD_SUBSTRING, substring);
+ }
+
+ @Operator
public Predicate<ProjectData> parent(String parentName) {
- return ProjectPredicates.parent(Project.nameKey(parentName));
+ if (!index.getSchema().hasField(ProjectField.PARENT_NAME_2_SPEC)) {
+ return ProjectPredicates.parent(Project.nameKey(parentName));
+ }
+ return ProjectPredicates.parent2(Project.nameKey(parentName));
}
@Operator
@@ -103,4 +125,17 @@
}
return new LimitPredicate<>(FIELD_LIMIT, limit);
}
+
+ private void checkOperatorAvailable(SchemaField<ProjectData, ?> field, String operator)
+ throws QueryParseException {
+ checkFieldAvailable(
+ field, String.format("'%s' operator is not supported on this gerrit host", operator));
+ }
+
+ private void checkFieldAvailable(SchemaField<ProjectData, ?> field, String errorMessage)
+ throws QueryParseException {
+ if (!index.getSchema().hasField(field)) {
+ throw new QueryParseException(errorMessage);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/RestApiModule.java b/java/com/google/gerrit/server/restapi/RestApiModule.java
index dffcf44..73991c9 100644
--- a/java/com/google/gerrit/server/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/RestApiModule.java
@@ -24,6 +24,12 @@
import com.google.gerrit.server.restapi.project.ProjectRestApiModule;
import com.google.inject.AbstractModule;
+/**
+ * Module to bind REST API endpoints.
+ *
+ * <p>Classes that are needed by the REST layer, but which are not REST API endpoints, should be
+ * bound in {@link RestModule}.
+ */
public class RestApiModule extends AbstractModule {
@Override
protected void configure() {
diff --git a/java/com/google/gerrit/server/restapi/RestModule.java b/java/com/google/gerrit/server/restapi/RestModule.java
new file mode 100644
index 0000000..0dcb4c8
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/RestModule.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2023 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.restapi;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.gpg.PublicKeyStore;
+import com.google.gerrit.gpg.UnimplementedPublicKeyStoreProvider;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.change.AddReviewersOp;
+import com.google.gerrit.server.change.AddToAttentionSetOp;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.DeleteChangeOp;
+import com.google.gerrit.server.change.DeleteReviewerByEmailOp;
+import com.google.gerrit.server.change.DeleteReviewerOp;
+import com.google.gerrit.server.change.EmailReviewComments;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
+import com.google.gerrit.server.change.ReviewerResource;
+import com.google.gerrit.server.change.SetCherryPickOp;
+import com.google.gerrit.server.change.SetCustomKeyedValuesOp;
+import com.google.gerrit.server.change.SetHashtagsOp;
+import com.google.gerrit.server.change.SetPrivateOp;
+import com.google.gerrit.server.change.SetTopicOp;
+import com.google.gerrit.server.change.WorkInProgressOp;
+import com.google.gerrit.server.comment.CommentContextLoader;
+import com.google.gerrit.server.config.GerritConfigListener;
+import com.google.gerrit.server.group.db.GroupsUpdate;
+import com.google.gerrit.server.project.RefValidationHelper;
+import com.google.gerrit.server.restapi.change.DeleteVoteOp;
+import com.google.gerrit.server.restapi.change.PostReviewOp;
+import com.google.gerrit.server.restapi.change.PreviewFix;
+import com.google.gerrit.server.restapi.project.CreateProject;
+import com.google.gerrit.server.restapi.project.ProjectNode;
+import com.google.gerrit.server.restapi.project.SetParent;
+import com.google.gerrit.server.util.AttentionSetEmail;
+import com.google.gerrit.server.validators.ProjectCreationValidationListener;
+import com.google.inject.Provides;
+import com.google.inject.multibindings.OptionalBinder;
+
+/**
+ * Module to bind classes that are needed but the REST layer, but which are not REST endpoints.
+ *
+ * <p>REST endpoints should be bound in {@link RestApiModule}.
+ */
+public class RestModule extends FactoryModule {
+ @Override
+ protected void configure() {
+ factory(AccountLoader.Factory.class);
+ factory(AddReviewersOp.Factory.class);
+ factory(AddToAttentionSetOp.Factory.class);
+ factory(AttentionSetEmail.Factory.class);
+ factory(ChangeInserter.Factory.class);
+ factory(ChangeResource.Factory.class);
+ factory(CommentContextLoader.Factory.class);
+ factory(DeleteChangeOp.Factory.class);
+ factory(DeleteReviewerByEmailOp.Factory.class);
+ factory(DeleteReviewerOp.Factory.class);
+ factory(DeleteVoteOp.Factory.class);
+ factory(EmailReviewComments.Factory.class);
+ factory(GroupsUpdate.Factory.class);
+ factory(PatchSetInserter.Factory.class);
+ factory(PostReviewOp.Factory.class);
+ factory(PreviewFix.Factory.class);
+ factory(ProjectNode.Factory.class);
+ factory(RebaseChangeOp.Factory.class);
+ factory(RefValidationHelper.Factory.class);
+ factory(RemoveFromAttentionSetOp.Factory.class);
+ factory(ReviewerResource.Factory.class);
+ factory(SetCherryPickOp.Factory.class);
+ factory(SetCustomKeyedValuesOp.Factory.class);
+ factory(SetHashtagsOp.Factory.class);
+ factory(SetPrivateOp.Factory.class);
+ factory(SetTopicOp.Factory.class);
+ factory(WorkInProgressOp.Factory.class);
+
+ DynamicSet.bind(binder(), GerritConfigListener.class).to(SetParent.class);
+ DynamicSet.bind(binder(), ProjectCreationValidationListener.class)
+ .to(CreateProject.ValidBranchListener.class);
+
+ OptionalBinder.newOptionalBinder(binder(), PublicKeyStore.class)
+ .setDefault()
+ .toProvider(UnimplementedPublicKeyStoreProvider.class);
+ }
+
+ @Provides
+ @ServerInitiated
+ AccountsUpdate provideServerInitiatedAccountsUpdate(
+ @AccountsUpdate.AccountsUpdateLoader.WithReindex
+ AccountsUpdate.AccountsUpdateLoader accountsUpdateFactory) {
+ return accountsUpdateFactory.createWithServerIdent();
+ }
+
+ @Provides
+ @UserInitiated
+ AccountsUpdate provideUserInitiatedAccountsUpdate(
+ @AccountsUpdate.AccountsUpdateLoader.WithReindex
+ AccountsUpdate.AccountsUpdateLoader accountsUpdateFactory,
+ IdentifiedUser currentUser) {
+ return accountsUpdateFactory.create(currentUser);
+ }
+
+ @Provides
+ @ServerInitiated
+ GroupsUpdate provideServerInitiatedGroupsUpdate(GroupsUpdate.Factory groupsUpdateFactory) {
+ return groupsUpdateFactory.createWithServerIdent();
+ }
+
+ @Provides
+ @UserInitiated
+ GroupsUpdate provideUserInitiatedGroupsUpdate(
+ GroupsUpdate.Factory groupsUpdateFactory, IdentifiedUser currentUser) {
+ return groupsUpdateFactory.create(currentUser);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/account/AccountRestApiModule.java b/java/com/google/gerrit/server/restapi/account/AccountRestApiModule.java
index d0b4470..a09e1bc 100644
--- a/java/com/google/gerrit/server/restapi/account/AccountRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/account/AccountRestApiModule.java
@@ -23,110 +23,82 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.gpg.PublicKeyStore;
-import com.google.gerrit.gpg.UnimplementedPublicKeyStoreProvider;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ServerInitiated;
-import com.google.gerrit.server.UserInitiated;
-import com.google.gerrit.server.account.AccountsUpdate;
-import com.google.inject.Provides;
-import com.google.inject.multibindings.OptionalBinder;
public class AccountRestApiModule extends RestApiModule {
@Override
protected void configure() {
- bind(AccountsCollection.class);
+ bind(AccountsCollection.class).to(AccountsCollectionImpl.class);
bind(Capabilities.class);
- OptionalBinder.newOptionalBinder(binder(), PublicKeyStore.class)
- .setDefault()
- .toProvider(UnimplementedPublicKeyStoreProvider.class);
+ bind(StarredChanges.Create.class);
DynamicMap.mapOf(binder(), ACCOUNT_KIND);
DynamicMap.mapOf(binder(), CAPABILITY_KIND);
DynamicMap.mapOf(binder(), EMAIL_KIND);
DynamicMap.mapOf(binder(), SSH_KEY_KIND);
- DynamicMap.mapOf(binder(), STARRED_CHANGE_KIND);
DynamicMap.mapOf(binder(), STAR_KIND);
+ DynamicMap.mapOf(binder(), STARRED_CHANGE_KIND);
create(ACCOUNT_KIND).to(CreateAccount.class);
put(ACCOUNT_KIND).to(PutAccount.class);
delete(ACCOUNT_KIND).to(DeleteAccount.class);
get(ACCOUNT_KIND).to(GetAccount.class);
- get(ACCOUNT_KIND, "detail").to(GetDetail.class);
- post(ACCOUNT_KIND, "index").to(Index.class);
- get(ACCOUNT_KIND, "name").to(GetName.class);
- put(ACCOUNT_KIND, "name").to(PutName.class);
- delete(ACCOUNT_KIND, "name").to(PutName.class);
- get(ACCOUNT_KIND, "status").to(GetStatus.class);
- put(ACCOUNT_KIND, "status").to(PutStatus.class);
- put(ACCOUNT_KIND, "displayname").to(PutDisplayName.class);
- get(ACCOUNT_KIND, "username").to(GetUsername.class);
- put(ACCOUNT_KIND, "username").to(PutUsername.class);
get(ACCOUNT_KIND, "active").to(GetActive.class);
put(ACCOUNT_KIND, "active").to(PutActive.class);
delete(ACCOUNT_KIND, "active").to(DeleteActive.class);
- child(ACCOUNT_KIND, "emails").to(EmailsCollection.class);
- create(EMAIL_KIND).to(CreateEmail.class);
- get(EMAIL_KIND).to(GetEmail.class);
- put(EMAIL_KIND).to(PutEmail.class);
- delete(EMAIL_KIND).to(DeleteEmail.class);
- put(EMAIL_KIND, "preferred").to(PutPreferred.class);
- put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
- delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
- get(ACCOUNT_KIND, "watched.projects").to(GetWatchedProjects.class);
- post(ACCOUNT_KIND, "watched.projects").to(PostWatchedProjects.class);
- post(ACCOUNT_KIND, "watched.projects:delete").to(DeleteWatchedProjects.class);
-
- child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
- postOnCollection(SSH_KEY_KIND).to(AddSshKey.class);
- get(SSH_KEY_KIND).to(GetSshKey.class);
- delete(SSH_KEY_KIND).to(DeleteSshKey.class);
-
+ get(ACCOUNT_KIND, "agreements").to(GetAgreements.class);
+ put(ACCOUNT_KIND, "agreements").to(PutAgreement.class);
get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);
child(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
+ get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
+ put(ACCOUNT_KIND, "displayname").to(PutDisplayName.class);
+ get(ACCOUNT_KIND, "detail").to(GetDetail.class);
+ post(ACCOUNT_KIND, "drafts:delete").to(DeleteDraftComments.class);
+
+ child(ACCOUNT_KIND, "emails").to(EmailsCollection.class);
+ create(EMAIL_KIND).to(CreateEmail.class);
+ delete(EMAIL_KIND).to(DeleteEmail.class);
+ get(EMAIL_KIND).to(GetEmail.class);
+ put(EMAIL_KIND).to(PutEmail.class);
+ put(EMAIL_KIND, "preferred").to(PutPreferred.class);
+
+ get(ACCOUNT_KIND, "external.ids").to(GetExternalIds.class);
+ post(ACCOUNT_KIND, "external.ids:delete").to(DeleteExternalIds.class);
get(ACCOUNT_KIND, "groups").to(GetGroups.class);
+ post(ACCOUNT_KIND, "index").to(Index.class);
+ get(ACCOUNT_KIND, "name").to(GetName.class);
+ put(ACCOUNT_KIND, "name").to(PutName.class);
+ delete(ACCOUNT_KIND, "name").to(PutName.class);
+ put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
+ delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
get(ACCOUNT_KIND, "preferences").to(GetPreferences.class);
put(ACCOUNT_KIND, "preferences").to(SetPreferences.class);
get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
get(ACCOUNT_KIND, "preferences.edit").to(GetEditPreferences.class);
put(ACCOUNT_KIND, "preferences.edit").to(SetEditPreferences.class);
- get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
- get(ACCOUNT_KIND, "agreements").to(GetAgreements.class);
- put(ACCOUNT_KIND, "agreements").to(PutAgreement.class);
+ child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
+ postOnCollection(SSH_KEY_KIND).to(AddSshKey.class);
+ get(SSH_KEY_KIND).to(GetSshKey.class);
+ delete(SSH_KEY_KIND).to(DeleteSshKey.class);
child(ACCOUNT_KIND, "starred.changes").to(StarredChanges.class);
create(STARRED_CHANGE_KIND).to(StarredChanges.Create.class);
put(STARRED_CHANGE_KIND).to(StarredChanges.Put.class);
delete(STARRED_CHANGE_KIND).to(StarredChanges.Delete.class);
- bind(StarredChanges.Create.class);
- get(ACCOUNT_KIND, "external.ids").to(GetExternalIds.class);
- post(ACCOUNT_KIND, "external.ids:delete").to(DeleteExternalIds.class);
-
- post(ACCOUNT_KIND, "drafts:delete").to(DeleteDraftComments.class);
+ get(ACCOUNT_KIND, "status").to(GetStatus.class);
+ put(ACCOUNT_KIND, "status").to(PutStatus.class);
+ get(ACCOUNT_KIND, "username").to(GetUsername.class);
+ put(ACCOUNT_KIND, "username").to(PutUsername.class);
+ get(ACCOUNT_KIND, "watched.projects").to(GetWatchedProjects.class);
+ post(ACCOUNT_KIND, "watched.projects").to(PostWatchedProjects.class);
+ post(ACCOUNT_KIND, "watched.projects:delete").to(DeleteWatchedProjects.class);
// The gpgkeys REST endpoints are bound via GpgApiModule.
- }
-
- @Provides
- @ServerInitiated
- AccountsUpdate provideServerInitiatedAccountsUpdate(
- @AccountsUpdate.AccountsUpdateLoader.WithReindex
- AccountsUpdate.AccountsUpdateLoader accountsUpdateFactory) {
- return accountsUpdateFactory.createWithServerIdent();
- }
-
- @Provides
- @UserInitiated
- AccountsUpdate provideUserInitiatedAccountsUpdate(
- @AccountsUpdate.AccountsUpdateLoader.WithReindex
- AccountsUpdate.AccountsUpdateLoader accountsUpdateFactory,
- IdentifiedUser currentUser) {
- return accountsUpdateFactory.create(currentUser);
+ // The oauthtoken REST endpoint is bound via OAuthRestModule.
}
}
diff --git a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
index 61ff6b8..fa919df 100644
--- a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
+++ b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2023 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.
@@ -21,52 +21,19 @@
import com.google.gerrit.extensions.restapi.RestCollection;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
import com.google.gerrit.server.account.AccountResource;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
-@Singleton
-public class AccountsCollection implements RestCollection<TopLevelResource, AccountResource> {
- private final AccountResolver accountResolver;
- private final Provider<QueryAccounts> list;
- private final DynamicMap<RestView<AccountResource>> views;
-
- @Inject
- public AccountsCollection(
- AccountResolver accountResolver,
- Provider<QueryAccounts> list,
- DynamicMap<RestView<AccountResource>> views) {
- this.accountResolver = accountResolver;
- this.list = list;
- this.views = views;
- }
+/** A generic interface for parsing account IDs from URL resources. */
+public interface AccountsCollection extends RestCollection<TopLevelResource, AccountResource> {
+ @Override
+ AccountResource parse(TopLevelResource root, IdString id)
+ throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException;
@Override
- public AccountResource parse(TopLevelResource root, IdString id)
- throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException {
- try {
- return new AccountResource(accountResolver.resolve(id.get()).asUniqueUser());
- } catch (UnresolvableAccountException e) {
- if (e.isSelf()) {
- // Must be authenticated to use 'me' or 'self'.
- throw new AuthException(e.getMessage(), e);
- }
- throw new ResourceNotFoundException(e.getMessage(), e);
- }
- }
+ RestView<TopLevelResource> list() throws ResourceNotFoundException;
@Override
- public RestView<TopLevelResource> list() throws ResourceNotFoundException {
- return list.get();
- }
-
- @Override
- public DynamicMap<RestView<AccountResource>> views() {
- return views;
- }
+ DynamicMap<RestView<AccountResource>> views();
}
diff --git a/java/com/google/gerrit/server/restapi/account/AccountsCollectionImpl.java b/java/com/google/gerrit/server/restapi/account/AccountsCollectionImpl.java
new file mode 100644
index 0000000..141b01a
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/account/AccountsCollectionImpl.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2012 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.restapi.account;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class AccountsCollectionImpl implements AccountsCollection {
+ private final AccountResolver accountResolver;
+ private final Provider<QueryAccounts> list;
+ private final DynamicMap<RestView<AccountResource>> views;
+
+ @Inject
+ public AccountsCollectionImpl(
+ AccountResolver accountResolver,
+ Provider<QueryAccounts> list,
+ DynamicMap<RestView<AccountResource>> views) {
+ this.accountResolver = accountResolver;
+ this.list = list;
+ this.views = views;
+ }
+
+ @Override
+ public AccountResource parse(TopLevelResource root, IdString id)
+ throws ResourceNotFoundException, AuthException, IOException, ConfigInvalidException {
+ try {
+ return new AccountResource(accountResolver.resolve(id.get()).asUniqueUser());
+ } catch (UnresolvableAccountException e) {
+ if (e.isSelf()) {
+ // Must be authenticated to use 'me' or 'self'.
+ throw new AuthException(e.getMessage(), e);
+ }
+ throw new ResourceNotFoundException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public RestView<TopLevelResource> list() throws ResourceNotFoundException {
+ return list.get();
+ }
+
+ @Override
+ public DynamicMap<RestView<AccountResource>> views() {
+ return views;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteAccount.java b/java/com/google/gerrit/server/restapi/account/DeleteAccount.java
index c5cbd62..9b4c0a6 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteAccount.java
@@ -159,12 +159,10 @@
user.getUserName().ifPresent(sshKeyCache::evict);
}
- private void deleteStarredChanges(Account.Id accountId)
- throws StarredChangesUtil.IllegalLabelException {
+ private void deleteStarredChanges(Account.Id accountId) {
ImmutableSet<Change.Id> staredChanges = starredChangesUtil.byAccountId(accountId, false);
for (Change.Id change : staredChanges) {
- starredChangesUtil.star(
- self.get().getAccountId(), change, StarredChangesUtil.Operation.REMOVE);
+ starredChangesUtil.unstar(self.get().getAccountId(), change);
}
}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java
index 875496e..cbbaac5 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java
@@ -32,6 +32,7 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeJson;
@@ -67,6 +68,8 @@
private final ChangeJson.Factory changeJsonFactory;
private final Provider<CommentJson> commentJsonProvider;
private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
+
private final PatchSetUtil psUtil;
@Inject
@@ -78,6 +81,7 @@
ChangeJson.Factory changeJsonFactory,
Provider<CommentJson> commentJsonProvider,
CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
PatchSetUtil psUtil) {
this.batchUpdateFactory = batchUpdateFactory;
this.queryBuilder = queryBuilder;
@@ -86,6 +90,7 @@
this.changeJsonFactory = changeJsonFactory;
this.commentJsonProvider = commentJsonProvider;
this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.psUtil = psUtil;
}
@@ -122,7 +127,7 @@
private Predicate<ChangeData> predicate(Account.Id accountId, String query)
throws BadRequestException {
- Predicate<ChangeData> hasDraft = ChangePredicates.draftBy(commentsUtil, accountId);
+ Predicate<ChangeData> hasDraft = ChangePredicates.draftBy(draftCommentsReader, accountId);
if (CharMatcher.whitespace().trimFrom(Strings.nullToEmpty(query)).isEmpty()) {
return hasDraft;
}
@@ -147,7 +152,8 @@
public boolean updateChange(ChangeContext ctx) throws PermissionBackendException {
ImmutableList.Builder<CommentInfo> comments = ImmutableList.builder();
boolean dirty = false;
- for (HumanComment c : commentsUtil.draftByChangeAuthor(ctx.getNotes(), accountId)) {
+ for (HumanComment c :
+ draftCommentsReader.getDraftsByChangeAndDraftAuthor(ctx.getNotes(), accountId)) {
dirty = true;
PatchSet.Id psId = PatchSet.id(ctx.getChange().getId(), c.key.patchSetId);
commentsUtil.setCommentCommitId(c, ctx.getChange(), psUtil.get(ctx.getNotes(), psId));
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index 173f24b..08b9bac 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -20,10 +20,8 @@
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -36,8 +34,6 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gerrit.server.StarredChangesUtil.MutuallyExclusiveLabelsException;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -72,9 +68,7 @@
throws RestApiException, PermissionBackendException, IOException {
IdentifiedUser user = parent.getUser();
ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
- if (starredChangesUtil
- .getLabels(user.getAccountId(), change.getId())
- .contains(StarredChangesUtil.DEFAULT_LABEL)) {
+ if (starredChangesUtil.isStarred(user.getAccountId(), change.getId())) {
return new AccountResource.StarredChange(user, change);
}
throw new ResourceNotFoundException(id);
@@ -130,12 +124,7 @@
}
try {
- starredChangesUtil.star(
- self.get().getAccountId(), change.getId(), StarredChangesUtil.Operation.ADD);
- } catch (MutuallyExclusiveLabelsException e) {
- throw new ResourceConflictException(e.getMessage());
- } catch (IllegalLabelException e) {
- throw new BadRequestException(e.getMessage());
+ starredChangesUtil.star(self.get().getAccountId(), change.getId());
} catch (DuplicateKeyException e) {
return Response.none();
}
@@ -174,12 +163,11 @@
@Override
public Response<?> apply(AccountResource.StarredChange rsrc, Input in)
- throws AuthException, IOException, IllegalLabelException {
+ throws AuthException, IOException {
if (!self.get().hasSameAccountId(rsrc.getUser())) {
throw new AuthException("not allowed remove starred change");
}
- starredChangesUtil.star(
- self.get().getAccountId(), rsrc.getChange().getId(), StarredChangesUtil.Operation.REMOVE);
+ starredChangesUtil.unstar(self.get().getAccountId(), rsrc.getChange().getId());
return Response.none();
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
index ff37fd2..75eaacf 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
@@ -14,10 +14,13 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -48,6 +51,7 @@
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.update.BatchUpdate;
@@ -83,6 +87,8 @@
private final PatchSetInserter.Factory patchSetInserterFactory;
private final Provider<InternalChangeQuery> queryProvider;
private final ZoneId serverZoneId;
+ private final ProjectCache projectCache;
+ private final ChangeUtil changeUtil;
@Inject
ApplyPatch(
@@ -93,7 +99,9 @@
BatchUpdate.Factory batchUpdateFactory,
PatchSetInserter.Factory patchSetInserterFactory,
Provider<InternalChangeQuery> queryProvider,
- @GerritPersonIdent PersonIdent myIdent) {
+ @GerritPersonIdent PersonIdent myIdent,
+ ProjectCache projectCache,
+ ChangeUtil changeUtil) {
this.jsonFactory = jsonFactory;
this.contributorAgreements = contributorAgreements;
this.user = user;
@@ -102,6 +110,8 @@
this.patchSetInserterFactory = patchSetInserterFactory;
this.queryProvider = queryProvider;
this.serverZoneId = myIdent.getZoneId();
+ this.projectCache = projectCache;
+ this.changeUtil = changeUtil;
}
@Override
@@ -112,6 +122,10 @@
contributorAgreements.check(project, rsrc.getUser());
BranchNameKey destBranch = rsrc.getChange().getDest();
+ if (input == null || input.patch == null || input.patch.patch == null) {
+ throw new BadRequestException("patch required");
+ }
+
try (Repository repo = gitManager.openRepository(project);
// This inserter and revwalk *must* be passed to any BatchUpdates
// created later on, to ensure the applied commit is flushed
@@ -179,18 +193,14 @@
input.author == null
? committerIdent
: new PersonIdent(input.author.name, input.author.email, now, serverZoneId);
- List<FooterLine> footerLines = latestPatchset.getFooterLines();
- String messageWithNoFooters =
- !Strings.isNullOrEmpty(input.commitMessage)
- ? input.commitMessage
- : removeFooters(latestPatchset.getFullMessage(), footerLines);
String commitMessage =
- ApplyPatchUtil.buildCommitMessage(
- messageWithNoFooters,
- footerLines,
- input.patch.patch,
+ buildFullCommitMessage(
+ project,
+ latestPatchset,
+ input,
ApplyPatchUtil.getResultPatch(repo, reader, baseCommit, revWalk.lookupTree(treeId)),
applyResult.getErrors());
+
ObjectId appliedCommit =
CommitUtil.createCommitWithTree(
oi, authorIdent, committerIdent, parents, commitMessage, treeId);
@@ -214,6 +224,42 @@
}
}
+ private String buildFullCommitMessage(
+ NameKey project,
+ RevCommit latestPatchset,
+ ApplyPatchPatchSetInput input,
+ String resultPatch,
+ List<org.eclipse.jgit.patch.PatchApplier.Result.Error> errors)
+ throws ResourceConflictException, BadRequestException {
+ boolean hasInputCommitMessage = !Strings.isNullOrEmpty(input.commitMessage);
+ String fullMessage =
+ hasInputCommitMessage ? input.commitMessage : latestPatchset.getFullMessage();
+ // Since we might add error information to the message, we need to split the footers from the
+ // actual description.
+ List<FooterLine> footerLines = FooterLine.fromMessage(fullMessage);
+ String messageWithNoFooters = removeFooters(fullMessage, footerLines);
+ if (FooterLine.getValues(footerLines, FOOTER_CHANGE_ID).isEmpty()) {
+ footerLines.add(
+ latestPatchset.getFooterLines().stream()
+ .filter(f -> f.matches(FOOTER_CHANGE_ID))
+ .findFirst()
+ .get());
+ }
+ String commitMessage =
+ ApplyPatchUtil.buildCommitMessage(
+ messageWithNoFooters, footerLines, input.patch.patch, resultPatch, errors);
+
+ boolean changeIdRequired =
+ projectCache
+ .get(project)
+ .orElseThrow(illegalState(project))
+ .is(BooleanProjectConfig.REQUIRE_CHANGE_ID);
+ changeUtil.ensureChangeIdIsCorrect(
+ changeIdRequired, changeUtil.getChangeIdsFromFooter(latestPatchset).get(0), commitMessage);
+
+ return commitMessage;
+ }
+
private static Change insertPatchSet(
BatchUpdate bu,
Repository git,
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
index 0f2aa3c..2ac24c6 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
@@ -29,203 +29,166 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.change.AddReviewersOp;
-import com.google.gerrit.server.change.AddToAttentionSetOp;
-import com.google.gerrit.server.change.ChangeInserter;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.DeleteChangeOp;
-import com.google.gerrit.server.change.DeleteReviewerByEmailOp;
-import com.google.gerrit.server.change.DeleteReviewerOp;
-import com.google.gerrit.server.change.EmailReviewComments;
-import com.google.gerrit.server.change.PatchSetInserter;
-import com.google.gerrit.server.change.RebaseChangeOp;
-import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
-import com.google.gerrit.server.change.ReviewerResource;
-import com.google.gerrit.server.change.SetCherryPickOp;
-import com.google.gerrit.server.change.SetCustomKeyedValuesOp;
-import com.google.gerrit.server.change.SetHashtagsOp;
-import com.google.gerrit.server.change.SetPrivateOp;
-import com.google.gerrit.server.change.SetTopicOp;
-import com.google.gerrit.server.change.WorkInProgressOp;
-import com.google.gerrit.server.comment.CommentContextLoader;
import com.google.gerrit.server.restapi.change.Reviewed.DeleteReviewed;
import com.google.gerrit.server.restapi.change.Reviewed.PutReviewed;
-import com.google.gerrit.server.util.AttentionSetEmail;
public class ChangeRestApiModule extends RestApiModule {
@Override
protected void configure() {
bind(ChangesCollection.class);
- bind(Revisions.class);
+ bind(Comments.class);
+ bind(DraftComments.class);
+ bind(Files.class);
+ bind(Fixes.class);
bind(Reviewers.class);
bind(RevisionReviewers.class);
- bind(DraftComments.class);
- bind(Comments.class);
+ bind(Revisions.class);
bind(RobotComments.class);
- bind(Fixes.class);
- bind(Files.class);
bind(Votes.class);
+ DynamicMap.mapOf(binder(), ATTENTION_SET_ENTRY_KIND);
DynamicMap.mapOf(binder(), CHANGE_KIND);
+ DynamicMap.mapOf(binder(), CHANGE_EDIT_KIND);
+ DynamicMap.mapOf(binder(), CHANGE_MESSAGE_KIND);
DynamicMap.mapOf(binder(), COMMENT_KIND);
- DynamicMap.mapOf(binder(), ROBOT_COMMENT_KIND);
- DynamicMap.mapOf(binder(), FIX_KIND);
DynamicMap.mapOf(binder(), DRAFT_COMMENT_KIND);
DynamicMap.mapOf(binder(), FILE_KIND);
+ DynamicMap.mapOf(binder(), FIX_KIND);
+ DynamicMap.mapOf(binder(), ROBOT_COMMENT_KIND);
DynamicMap.mapOf(binder(), REVIEWER_KIND);
DynamicMap.mapOf(binder(), REVISION_KIND);
- DynamicMap.mapOf(binder(), CHANGE_EDIT_KIND);
DynamicMap.mapOf(binder(), VOTE_KIND);
- DynamicMap.mapOf(binder(), CHANGE_MESSAGE_KIND);
- DynamicMap.mapOf(binder(), ATTENTION_SET_ENTRY_KIND);
postOnCollection(CHANGE_KIND).to(CreateChange.class);
+ delete(CHANGE_KIND).to(DeleteChange.class);
get(CHANGE_KIND).to(GetChange.class);
- get(CHANGE_KIND, "meta_diff").to(GetMetaDiff.class);
- post(CHANGE_KIND, "merge").to(CreateMergePatchSet.class);
- get(CHANGE_KIND, "detail").to(GetDetail.class);
- get(CHANGE_KIND, "topic").to(GetTopic.class);
- get(CHANGE_KIND, "in").to(ChangeIncludedIn.class);
+ post(CHANGE_KIND, "abandon").to(Abandon.class);
+
child(CHANGE_KIND, "attention").to(AttentionSet.class);
+ postOnCollection(ATTENTION_SET_ENTRY_KIND).to(AddToAttentionSet.class);
delete(ATTENTION_SET_ENTRY_KIND).to(RemoveFromAttentionSet.class);
post(ATTENTION_SET_ENTRY_KIND, "delete").to(RemoveFromAttentionSet.class);
- postOnCollection(ATTENTION_SET_ENTRY_KIND).to(AddToAttentionSet.class);
- get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
- get(CHANGE_KIND, "custom_keyed_values").to(GetCustomKeyedValues.class);
- get(CHANGE_KIND, "comments").to(ListChangeComments.class);
- get(CHANGE_KIND, "robotcomments").to(ListChangeRobotComments.class);
- get(CHANGE_KIND, "drafts").to(ListChangeDrafts.class);
+
get(CHANGE_KIND, "check").to(Check.class);
- get(CHANGE_KIND, "pure_revert").to(GetPureRevert.class);
post(CHANGE_KIND, "check").to(Check.class);
- put(CHANGE_KIND, "topic").to(PutTopic.class);
- delete(CHANGE_KIND, "topic").to(PutTopic.class);
- delete(CHANGE_KIND).to(DeleteChange.class);
- post(CHANGE_KIND, "abandon").to(Abandon.class);
- post(CHANGE_KIND, "hashtags").to(PostHashtags.class);
- post(CHANGE_KIND, "custom_keyed_values").to(PostCustomKeyedValues.class);
- post(CHANGE_KIND, "restore").to(Restore.class);
- post(CHANGE_KIND, "revert").to(Revert.class);
- post(CHANGE_KIND, "revert_submission").to(RevertSubmission.class);
- post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
- get(CHANGE_KIND, "submitted_together").to(SubmittedTogether.class);
- post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
- post(CHANGE_KIND, "rebase:chain").to(RebaseChain.class);
- post(CHANGE_KIND, "index").to(Index.class);
- post(CHANGE_KIND, "move").to(Move.class);
- post(CHANGE_KIND, "private").to(PostPrivate.class);
- post(CHANGE_KIND, "private.delete").to(DeletePrivateByPost.class);
- delete(CHANGE_KIND, "private").to(DeletePrivate.class);
- post(CHANGE_KIND, "wip").to(SetWorkInProgress.class);
- post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
- put(CHANGE_KIND, "message").to(PutMessage.class);
post(CHANGE_KIND, "check.submit_requirement").to(CheckSubmitRequirement.class);
- post(CHANGE_KIND, "patch:apply").to(ApplyPatch.class);
-
- get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
- child(CHANGE_KIND, "reviewers").to(Reviewers.class);
- postOnCollection(REVIEWER_KIND).to(PostReviewers.class);
- get(REVIEWER_KIND).to(GetReviewer.class);
- delete(REVIEWER_KIND).to(DeleteReviewer.class);
- post(REVIEWER_KIND, "delete").to(DeleteReviewer.class);
- child(REVIEWER_KIND, "votes").to(Votes.class);
- delete(VOTE_KIND).to(DeleteVote.class);
- post(VOTE_KIND, "delete").to(DeleteVote.class);
-
- child(CHANGE_KIND, "revisions").to(Revisions.class);
- get(REVISION_KIND, "actions").to(GetRevisionActions.class);
- post(REVISION_KIND, "cherrypick").to(CherryPick.class);
- get(REVISION_KIND, "commit").to(GetCommit.class);
- get(REVISION_KIND, "mergeable").to(Mergeable.class);
- get(REVISION_KIND, "related").to(GetRelated.class);
- get(REVISION_KIND, "review").to(GetReview.class);
- post(REVISION_KIND, "review").to(PostReview.class);
- post(REVISION_KIND, "submit").to(Submit.class);
- post(REVISION_KIND, "rebase").to(Rebase.class);
- put(REVISION_KIND, "description").to(PutDescription.class);
- get(REVISION_KIND, "description").to(GetDescription.class);
- get(REVISION_KIND, "patch").to(GetPatch.class);
- get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
- post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
- post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
- get(REVISION_KIND, "archive").to(GetArchive.class);
- get(REVISION_KIND, "mergelist").to(GetMergeList.class);
-
- child(REVISION_KIND, "reviewers").to(RevisionReviewers.class);
-
- child(REVISION_KIND, "drafts").to(DraftComments.class);
- put(REVISION_KIND, "drafts").to(CreateDraftComment.class);
- get(DRAFT_COMMENT_KIND).to(GetDraftComment.class);
- put(DRAFT_COMMENT_KIND).to(PutDraftComment.class);
- delete(DRAFT_COMMENT_KIND).to(DeleteDraftComment.class);
-
- child(REVISION_KIND, "comments").to(Comments.class);
- get(COMMENT_KIND).to(GetComment.class);
- delete(COMMENT_KIND).to(DeleteComment.class);
- post(COMMENT_KIND, "delete").to(DeleteComment.class);
-
- child(REVISION_KIND, "robotcomments").to(RobotComments.class);
- get(ROBOT_COMMENT_KIND).to(GetRobotComment.class);
- child(REVISION_KIND, "fixes").to(Fixes.class);
- post(FIX_KIND, "apply").to(ApplyStoredFix.class);
- get(FIX_KIND, "preview").to(PreviewFix.Stored.class);
- post(REVISION_KIND, "fix:apply").to(ApplyProvidedFix.class);
- post(REVISION_KIND, "fix:preview").to(PreviewFix.Provided.class);
-
- get(REVISION_KIND, "ported_comments").to(ListPortedComments.class);
- get(REVISION_KIND, "ported_drafts").to(ListPortedDrafts.class);
-
- child(REVISION_KIND, "files").to(Files.class);
- put(FILE_KIND, "reviewed").to(PutReviewed.class);
- delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
- get(FILE_KIND, "content").to(GetContent.class);
- get(FILE_KIND, "download").to(DownloadContent.class);
- get(FILE_KIND, "diff").to(GetDiff.class);
- get(FILE_KIND, "blame").to(GetBlame.class);
+ get(CHANGE_KIND, "comments").to(ListChangeComments.class);
+ get(CHANGE_KIND, "custom_keyed_values").to(GetCustomKeyedValues.class);
+ post(CHANGE_KIND, "custom_keyed_values").to(PostCustomKeyedValues.class);
+ get(CHANGE_KIND, "detail").to(GetDetail.class);
+ get(CHANGE_KIND, "drafts").to(ListChangeDrafts.class);
child(CHANGE_KIND, "edit").to(ChangeEdits.class);
create(CHANGE_EDIT_KIND).to(ChangeEdits.Create.class);
+ delete(CHANGE_EDIT_KIND).to(ChangeEdits.DeleteContent.class);
+ deleteOnCollection(CHANGE_EDIT_KIND).to(DeleteChangeEdit.class);
deleteMissing(CHANGE_EDIT_KIND).to(ChangeEdits.DeleteFile.class);
postOnCollection(CHANGE_EDIT_KIND).to(ChangeEdits.Post.class);
- deleteOnCollection(CHANGE_EDIT_KIND).to(DeleteChangeEdit.class);
- post(CHANGE_KIND, "edit:publish").to(PublishChangeEdit.class);
- post(CHANGE_KIND, "edit:rebase").to(RebaseChangeEdit.class);
- put(CHANGE_KIND, "edit:message").to(ChangeEdits.EditMessage.class);
- get(CHANGE_KIND, "edit:message").to(ChangeEdits.GetMessage.class);
- put(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Put.class);
- delete(CHANGE_EDIT_KIND).to(ChangeEdits.DeleteContent.class);
get(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Get.class);
+ put(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Put.class);
get(CHANGE_EDIT_KIND, "meta").to(ChangeEdits.GetMeta.class);
+ put(CHANGE_KIND, "edit:message").to(ChangeEdits.EditMessage.class);
+ get(CHANGE_KIND, "edit:message").to(ChangeEdits.GetMessage.class);
+ post(CHANGE_KIND, "edit:publish").to(PublishChangeEdit.class);
+ post(CHANGE_KIND, "edit:rebase").to(RebaseChangeEdit.class);
+ get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
+ post(CHANGE_KIND, "hashtags").to(PostHashtags.class);
+ get(CHANGE_KIND, "in").to(ChangeIncludedIn.class);
+ post(CHANGE_KIND, "index").to(Index.class);
+ get(CHANGE_KIND, "meta_diff").to(GetMetaDiff.class);
+ post(CHANGE_KIND, "merge").to(CreateMergePatchSet.class);
+ put(CHANGE_KIND, "message").to(PutMessage.class);
+
child(CHANGE_KIND, "messages").to(ChangeMessages.class);
- get(CHANGE_MESSAGE_KIND).to(GetChangeMessage.class);
delete(CHANGE_MESSAGE_KIND).to(DeleteChangeMessage.DefaultDeleteChangeMessage.class);
+ get(CHANGE_MESSAGE_KIND).to(GetChangeMessage.class);
post(CHANGE_MESSAGE_KIND, "delete").to(DeleteChangeMessage.class);
- factory(AccountLoader.Factory.class);
- factory(ChangeInserter.Factory.class);
- factory(ChangeResource.Factory.class);
- factory(CommentContextLoader.Factory.class);
- factory(DeleteChangeOp.Factory.class);
- factory(DeleteReviewerByEmailOp.Factory.class);
- factory(DeleteReviewerOp.Factory.class);
- factory(DeleteVoteOp.Factory.class);
- factory(EmailReviewComments.Factory.class);
- factory(PatchSetInserter.Factory.class);
- factory(AddReviewersOp.Factory.class);
- factory(PostReviewOp.Factory.class);
- factory(PreviewFix.Factory.class);
- factory(RebaseChangeOp.Factory.class);
- factory(ReviewerResource.Factory.class);
- factory(SetCherryPickOp.Factory.class);
- factory(SetCustomKeyedValuesOp.Factory.class);
- factory(SetHashtagsOp.Factory.class);
- factory(SetTopicOp.Factory.class);
- factory(SetPrivateOp.Factory.class);
- factory(WorkInProgressOp.Factory.class);
- factory(AddToAttentionSetOp.Factory.class);
- factory(RemoveFromAttentionSetOp.Factory.class);
- factory(AttentionSetEmail.Factory.class);
+ post(CHANGE_KIND, "move").to(Move.class);
+ post(CHANGE_KIND, "patch:apply").to(ApplyPatch.class);
+ post(CHANGE_KIND, "private").to(PostPrivate.class);
+ post(CHANGE_KIND, "private.delete").to(DeletePrivateByPost.class);
+ delete(CHANGE_KIND, "private").to(DeletePrivate.class);
+ get(CHANGE_KIND, "pure_revert").to(GetPureRevert.class);
+ post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
+ post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
+ post(CHANGE_KIND, "rebase:chain").to(RebaseChain.class);
+ post(CHANGE_KIND, "restore").to(Restore.class);
+ post(CHANGE_KIND, "revert").to(Revert.class);
+ post(CHANGE_KIND, "revert_submission").to(RevertSubmission.class);
+
+ child(CHANGE_KIND, "reviewers").to(Reviewers.class);
+ postOnCollection(REVIEWER_KIND).to(PostReviewers.class);
+ delete(REVIEWER_KIND).to(DeleteReviewer.class);
+ get(REVIEWER_KIND).to(GetReviewer.class);
+ post(REVIEWER_KIND, "delete").to(DeleteReviewer.class);
+ child(REVIEWER_KIND, "votes").to(Votes.class);
+
+ child(CHANGE_KIND, "revisions").to(Revisions.class);
+ get(REVISION_KIND, "actions").to(GetRevisionActions.class);
+ get(REVISION_KIND, "archive").to(GetArchive.class);
+ post(REVISION_KIND, "cherrypick").to(CherryPick.class);
+
+ child(REVISION_KIND, "comments").to(Comments.class);
+ delete(COMMENT_KIND).to(DeleteComment.class);
+ get(COMMENT_KIND).to(GetComment.class);
+ post(COMMENT_KIND, "delete").to(DeleteComment.class);
+
+ get(REVISION_KIND, "commit").to(GetCommit.class);
+ get(REVISION_KIND, "description").to(GetDescription.class);
+ put(REVISION_KIND, "description").to(PutDescription.class);
+
+ child(REVISION_KIND, "drafts").to(DraftComments.class);
+ put(REVISION_KIND, "drafts").to(CreateDraftComment.class);
+ delete(DRAFT_COMMENT_KIND).to(DeleteDraftComment.class);
+ get(DRAFT_COMMENT_KIND).to(GetDraftComment.class);
+ put(DRAFT_COMMENT_KIND).to(PutDraftComment.class);
+
+ child(REVISION_KIND, "files").to(Files.class);
+ get(FILE_KIND, "blame").to(GetBlame.class);
+ get(FILE_KIND, "content").to(GetContent.class);
+ get(FILE_KIND, "diff").to(GetDiff.class);
+ get(FILE_KIND, "download").to(DownloadContent.class);
+ put(FILE_KIND, "reviewed").to(PutReviewed.class);
+ delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
+
+ child(REVISION_KIND, "fixes").to(Fixes.class);
+ post(FIX_KIND, "apply").to(ApplyStoredFix.class);
+ get(FIX_KIND, "preview").to(PreviewFix.Stored.class);
+
+ post(REVISION_KIND, "fix:apply").to(ApplyProvidedFix.class);
+ post(REVISION_KIND, "fix:preview").to(PreviewFix.Provided.class);
+ get(REVISION_KIND, "mergeable").to(Mergeable.class);
+ get(REVISION_KIND, "mergelist").to(GetMergeList.class);
+ get(REVISION_KIND, "patch").to(GetPatch.class);
+ get(REVISION_KIND, "ported_comments").to(ListPortedComments.class);
+ get(REVISION_KIND, "ported_drafts").to(ListPortedDrafts.class);
+ post(REVISION_KIND, "rebase").to(Rebase.class);
+ get(REVISION_KIND, "related").to(GetRelated.class);
+ get(REVISION_KIND, "review").to(GetReview.class);
+ post(REVISION_KIND, "review").to(PostReview.class);
+ child(REVISION_KIND, "reviewers").to(RevisionReviewers.class);
+
+ child(REVISION_KIND, "robotcomments").to(RobotComments.class);
+ get(ROBOT_COMMENT_KIND).to(GetRobotComment.class);
+
+ post(REVISION_KIND, "submit").to(Submit.class);
+ get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
+ post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
+ post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
+
+ get(CHANGE_KIND, "robotcomments").to(ListChangeRobotComments.class);
+ delete(CHANGE_KIND, "topic").to(PutTopic.class);
+ get(CHANGE_KIND, "topic").to(GetTopic.class);
+ put(CHANGE_KIND, "topic").to(PutTopic.class);
+ post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
+ get(CHANGE_KIND, "submitted_together").to(SubmittedTogether.class);
+ get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
+
+ delete(VOTE_KIND).to(DeleteVote.class);
+ post(VOTE_KIND, "delete").to(DeleteVote.class);
+
+ post(CHANGE_KIND, "wip").to(SetWorkInProgress.class);
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 51094b7..ff21916 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -19,6 +19,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
@@ -181,9 +182,10 @@
Instant now = TimeUtil.now();
IdentifiedUser me = user.get().asIdentifiedUser();
+ PersonIdent committer = me.newCommitterIdent(now, serverZoneId);
PersonIdent author =
in.author == null
- ? me.newCommitterIdent(now, serverZoneId)
+ ? committer
: new PersonIdent(in.author.name, in.author.email, now, serverZoneId);
CodeReviewCommit newCommit =
createMergeCommit(
@@ -196,6 +198,7 @@
currentPsCommit,
sourceCommit,
author,
+ committer,
ObjectId.fromString(change.getKey().get().substring(1)));
oi.flush();
@@ -210,6 +213,16 @@
.setMessage(messageForChange(nextPsId, newCommit))
.setWorkInProgress(!newCommit.getFilesWithGitConflicts().isEmpty())
.setCheckAddPatchSetPermission(false);
+
+ if (in.validationOptions != null) {
+ ImmutableListMultimap.Builder<String, String> validationOptions =
+ ImmutableListMultimap.builder();
+ in.validationOptions
+ .entrySet()
+ .forEach(e -> validationOptions.put(e.getKey(), e.getValue()));
+ psInserter.setValidationOptions(validationOptions.build());
+ }
+
if (groups != null) {
psInserter.setGroups(groups);
}
@@ -253,6 +266,7 @@
RevCommit currentPsCommit,
RevCommit sourceCommit,
PersonIdent author,
+ PersonIdent committer,
ObjectId changeId)
throws ResourceNotFoundException, MergeIdenticalTreeException, MergeConflictException,
IOException {
@@ -295,6 +309,7 @@
mergeStrategy,
in.merge.allowConflicts,
author,
+ committer,
commitMsg,
rw);
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index f55e9c7..4e17de3 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -26,6 +26,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.DraftCommentResource;
import com.google.gerrit.server.update.BatchUpdate;
@@ -43,13 +44,19 @@
public class DeleteDraftComment implements RestModifyView<DraftCommentResource, Input> {
private final BatchUpdate.Factory updateFactory;
private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
+
private final PatchSetUtil psUtil;
@Inject
DeleteDraftComment(
- BatchUpdate.Factory updateFactory, CommentsUtil commentsUtil, PatchSetUtil psUtil) {
+ BatchUpdate.Factory updateFactory,
+ CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
+ PatchSetUtil psUtil) {
this.updateFactory = updateFactory;
this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.psUtil = psUtil;
}
@@ -77,7 +84,7 @@
@Override
public boolean updateChange(ChangeContext ctx) throws ResourceNotFoundException {
Optional<HumanComment> maybeComment =
- commentsUtil.getDraft(ctx.getNotes(), ctx.getIdentifiedUser(), key);
+ draftCommentsReader.getDraftComment(ctx.getNotes(), ctx.getIdentifiedUser(), key);
if (!maybeComment.isPresent()) {
return false; // Nothing to do.
}
diff --git a/java/com/google/gerrit/server/restapi/change/DownloadContent.java b/java/com/google/gerrit/server/restapi/change/DownloadContent.java
index 74c0acc..ae801b4 100644
--- a/java/com/google/gerrit/server/restapi/change/DownloadContent.java
+++ b/java/com/google/gerrit/server/restapi/change/DownloadContent.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
@@ -44,7 +45,7 @@
@Override
public Response<BinaryResult> apply(FileResource rsrc)
- throws ResourceNotFoundException, IOException, NoSuchChangeException {
+ throws BadRequestException, ResourceNotFoundException, IOException, NoSuchChangeException {
String path = rsrc.getPatchKey().fileName();
RevisionResource rev = rsrc.getRevision();
return Response.ok(
diff --git a/java/com/google/gerrit/server/restapi/change/DraftComments.java b/java/com/google/gerrit/server/restapi/change/DraftComments.java
index a4c9400..7c4f596 100644
--- a/java/com/google/gerrit/server/restapi/change/DraftComments.java
+++ b/java/com/google/gerrit/server/restapi/change/DraftComments.java
@@ -21,8 +21,8 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.change.DraftCommentResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.inject.Inject;
@@ -34,18 +34,18 @@
private final DynamicMap<RestView<DraftCommentResource>> views;
private final Provider<CurrentUser> user;
private final ListRevisionDrafts list;
- private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
@Inject
DraftComments(
DynamicMap<RestView<DraftCommentResource>> views,
Provider<CurrentUser> user,
ListRevisionDrafts list,
- CommentsUtil commentsUtil) {
+ DraftCommentsReader draftCommentsReader) {
this.views = views;
this.user = user;
this.list = list;
- this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
}
@Override
@@ -65,8 +65,8 @@
checkIdentifiedUser();
String uuid = id.get();
for (HumanComment c :
- commentsUtil.draftByPatchSetAuthor(
- rev.getPatchSet().id(), rev.getAccountId(), rev.getNotes())) {
+ draftCommentsReader.getDraftsByPatchSetAndDraftAuthor(
+ rev.getNotes(), rev.getPatchSet().id(), rev.getAccountId())) {
if (uuid.equals(c.key.uuid)) {
return new DraftCommentResource(rev, c);
}
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
index 89ee399..9faa9b5 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeDrafts.java
@@ -20,7 +20,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.change.ChangeData;
@@ -34,7 +34,7 @@
public class ListChangeDrafts implements RestReadView<ChangeResource> {
private final ChangeData.Factory changeDataFactory;
private final Provider<CommentJson> commentJson;
- private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
private boolean includeContext;
private int contextPadding;
@@ -64,15 +64,16 @@
ListChangeDrafts(
ChangeData.Factory changeDataFactory,
Provider<CommentJson> commentJson,
- CommentsUtil commentsUtil) {
+ DraftCommentsReader draftCommentsReader) {
this.changeDataFactory = changeDataFactory;
this.commentJson = commentJson;
- this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
}
private Iterable<HumanComment> listComments(ChangeResource rsrc) {
ChangeData cd = changeDataFactory.create(rsrc.getNotes());
- return commentsUtil.draftByChangeAuthor(cd.notes(), rsrc.getUser().getAccountId());
+ return draftCommentsReader.getDraftsByChangeAndDraftAuthor(
+ cd.notes(), rsrc.getUser().getAccountId());
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/change/ListPortedDrafts.java b/java/com/google/gerrit/server/restapi/change/ListPortedDrafts.java
index e92fe5c..f3d5f37 100644
--- a/java/com/google/gerrit/server/restapi/change/ListPortedDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListPortedDrafts.java
@@ -22,7 +22,7 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.inject.Inject;
@@ -34,15 +34,17 @@
@Singleton
public class ListPortedDrafts implements RestReadView<RevisionResource> {
- private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
private final CommentPorter commentPorter;
private final Provider<CommentJson> commentJson;
@Inject
public ListPortedDrafts(
- Provider<CommentJson> commentJson, CommentsUtil commentsUtil, CommentPorter commentPorter) {
+ Provider<CommentJson> commentJson,
+ DraftCommentsReader draftCommentsReader,
+ CommentPorter commentPorter) {
this.commentJson = commentJson;
- this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.commentPorter = commentPorter;
}
@@ -55,7 +57,7 @@
PatchSet targetPatchset = revisionResource.getPatchSet();
List<HumanComment> draftComments =
- commentsUtil.draftByChangeAuthor(
+ draftCommentsReader.getDraftsByChangeAndDraftAuthor(
revisionResource.getNotes(), revisionResource.getAccountId());
ImmutableList<HumanComment> portedDraftComments =
commentPorter.portComments(
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java b/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
index 88309ed..d969a9a 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionComments.java
@@ -16,6 +16,7 @@
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
@@ -24,9 +25,15 @@
@Singleton
public class ListRevisionComments extends ListRevisionDrafts {
+ private final CommentsUtil commentsUtil;
+
@Inject
- ListRevisionComments(Provider<CommentJson> commentJson, CommentsUtil commentsUtil) {
- super(commentJson, commentsUtil);
+ ListRevisionComments(
+ Provider<CommentJson> commentJson,
+ CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader) {
+ super(commentJson, draftCommentsReader);
+ this.commentsUtil = commentsUtil;
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
index a5fbd92..de05a16 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionDrafts.java
@@ -19,7 +19,7 @@
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.inject.Inject;
@@ -31,17 +31,17 @@
@Singleton
public class ListRevisionDrafts implements RestReadView<RevisionResource> {
protected final Provider<CommentJson> commentJson;
- protected final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
@Inject
- ListRevisionDrafts(Provider<CommentJson> commentJson, CommentsUtil commentsUtil) {
+ ListRevisionDrafts(Provider<CommentJson> commentJson, DraftCommentsReader draftCommentsReader) {
this.commentJson = commentJson;
- this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
}
protected Iterable<HumanComment> listComments(RevisionResource rsrc) {
- return commentsUtil.draftByPatchSetAuthor(
- rsrc.getPatchSet().id(), rsrc.getAccountId(), rsrc.getNotes());
+ return draftCommentsReader.getDraftsByPatchSetAndDraftAuthor(
+ rsrc.getNotes(), rsrc.getPatchSet().id(), rsrc.getAccountId());
}
protected boolean includeAuthorInfo() {
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index c3688d6..2b0de12 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -286,6 +286,9 @@
.setTitle("Move change to a different branch")
.setVisible(false);
+ if (!moveEnabled) {
+ return description;
+ }
Change change = rsrc.getChange();
if (!change.isNew()) {
return description;
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 9940637..32474a4 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -72,12 +72,14 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.approval.ApprovalsUtil;
+import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.ModifyReviewersEmail;
import com.google.gerrit.server.change.NotifyResolver;
@@ -163,6 +165,8 @@
private final AccountCache accountCache;
private final ApprovalsUtil approvalsUtil;
private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
+
private final PatchListCache patchListCache;
private final AccountResolver accountResolver;
private final ReviewerModifier reviewerModifier;
@@ -176,6 +180,7 @@
private final ReplyAttentionSetUpdates replyAttentionSetUpdates;
private final ReviewerAdded reviewerAdded;
private final boolean strictLabels;
+ private final ChangeJson.Factory changeJsonFactory;
@Inject
PostReview(
@@ -186,6 +191,7 @@
AccountCache accountCache,
ApprovalsUtil approvalsUtil,
CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
PatchListCache patchListCache,
AccountResolver accountResolver,
ReviewerModifier reviewerModifier,
@@ -197,13 +203,15 @@
ProjectCache projectCache,
PermissionBackend permissionBackend,
ReplyAttentionSetUpdates replyAttentionSetUpdates,
- ReviewerAdded reviewerAdded) {
+ ReviewerAdded reviewerAdded,
+ ChangeJson.Factory changeJsonFactory) {
this.updateFactory = updateFactory;
this.postReviewOpFactory = postReviewOpFactory;
this.changeResourceFactory = changeResourceFactory;
this.changeDataFactory = changeDataFactory;
this.accountCache = accountCache;
this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.patchListCache = patchListCache;
this.approvalsUtil = approvalsUtil;
this.accountResolver = accountResolver;
@@ -217,6 +225,7 @@
this.replyAttentionSetUpdates = replyAttentionSetUpdates;
this.reviewerAdded = reviewerAdded;
this.strictLabels = gerritConfig.getBoolean("change", "strictLabels", false);
+ this.changeJsonFactory = changeJsonFactory;
}
@Override
@@ -404,6 +413,10 @@
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);
+ }
+
return Response.ok(output);
}
@@ -685,7 +698,8 @@
RevisionResource resource, List<String> draftIds, DraftHandling draftHandling)
throws BadRequestException {
Map<String, HumanComment> draftsByUuid =
- commentsUtil.draftByChangeAuthor(resource.getNotes(), resource.getUser().getAccountId())
+ draftCommentsReader
+ .getDraftsByChangeAndDraftAuthor(resource.getNotes(), resource.getUser().getAccountId())
.stream()
.collect(Collectors.toMap(c -> c.key.uuid, c -> c));
List<String> nonExistingDraftIds =
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
index db6fa77..a47e179 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
@@ -59,6 +59,7 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil;
@@ -96,7 +97,7 @@
import org.eclipse.jgit.lib.Config;
public class PostReviewOp implements BatchUpdateOp {
- interface Factory {
+ public interface Factory {
PostReviewOp create(
ProjectState projectState, PatchSet.Id psId, ReviewInput in, Account.Id reviewerId);
}
@@ -183,6 +184,7 @@
private final ApprovalsUtil approvalsUtil;
private final ChangeMessagesUtil cmUtil;
private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
private final PublishCommentUtil publishCommentUtil;
private final PatchSetUtil psUtil;
private final EmailReviewComments.Factory email;
@@ -214,6 +216,7 @@
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
PublishCommentUtil publishCommentUtil,
PatchSetUtil psUtil,
EmailReviewComments.Factory email,
@@ -230,6 +233,7 @@
this.psUtil = psUtil;
this.cmUtil = cmUtil;
this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.email = email;
this.commentAdded = commentAdded;
this.commentValidators = commentValidators;
@@ -402,7 +406,7 @@
inputComment.unresolved,
parent);
} else {
- // In ChangeUpdate#putComment() the draft with the same ID will be deleted.
+ // In ChangeUpdate#putDraftComment() the draft with the same ID will be deleted.
comment.writtenOn = Timestamp.from(ctx.getWhen());
comment.side = inputComment.side();
comment.message = inputComment.message;
@@ -552,12 +556,14 @@
}
private Map<String, HumanComment> changeDrafts(ChangeContext ctx) {
- return commentsUtil.draftByChangeAuthor(ctx.getNotes(), user.getAccountId()).stream()
+ return draftCommentsReader.getDraftsByChangeAndDraftAuthor(ctx.getNotes(), user.getAccountId())
+ .stream()
.collect(Collectors.toMap(c -> c.key.uuid, c -> c));
}
private Map<String, HumanComment> patchSetDrafts(ChangeContext ctx) {
- return commentsUtil.draftByPatchSetAuthor(psId, user.getAccountId(), ctx.getNotes()).stream()
+ return draftCommentsReader
+ .getDraftsByPatchSetAndDraftAuthor(ctx.getNotes(), psId, user.getAccountId()).stream()
.collect(Collectors.toMap(c -> c.key.uuid, c -> c));
}
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 0a3e31f..345d915 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -30,6 +30,7 @@
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.extensions.validators.CommentValidator;
import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.DraftCommentResource;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -54,6 +55,7 @@
private final BatchUpdate.Factory updateFactory;
private final DeleteDraftComment delete;
private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
private final PatchSetUtil psUtil;
private final Provider<CommentJson> commentJson;
private final ChangeNotes.Factory changeNotesFactory;
@@ -64,6 +66,7 @@
BatchUpdate.Factory updateFactory,
DeleteDraftComment delete,
CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader,
PatchSetUtil psUtil,
Provider<CommentJson> commentJson,
ChangeNotes.Factory changeNotesFactory,
@@ -71,6 +74,7 @@
this.updateFactory = updateFactory;
this.delete = delete;
this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
this.psUtil = psUtil;
this.commentJson = commentJson;
this.changeNotesFactory = changeNotesFactory;
@@ -125,7 +129,7 @@
@Override
public boolean updateChange(ChangeContext ctx) throws ResourceNotFoundException {
Optional<HumanComment> maybeComment =
- commentsUtil.getDraft(ctx.getNotes(), ctx.getIdentifiedUser(), key);
+ draftCommentsReader.getDraftComment(ctx.getNotes(), ctx.getIdentifiedUser(), key);
if (!maybeComment.isPresent()) {
// Disappeared out from under us. Can't easily fall back to insert,
// because the input might be missing required fields. Just give up.
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index 41710a6..4eca1f3 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -21,7 +21,6 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.common.CommitMessageInput;
-import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -35,7 +34,6 @@
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
-import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
@@ -74,7 +72,7 @@
private final PatchSetUtil psUtil;
private final NotifyResolver notifyResolver;
private final ProjectCache projectCache;
- private final DynamicItem<UrlFormatter> urlFormatter;
+ private final ChangeUtil changeUtil;
@Inject
PutMessage(
@@ -87,7 +85,7 @@
PatchSetUtil psUtil,
NotifyResolver notifyResolver,
ProjectCache projectCache,
- DynamicItem<UrlFormatter> urlFormatter) {
+ ChangeUtil changeUtil) {
this.updateFactory = updateFactory;
this.repositoryManager = repositoryManager;
this.userProvider = userProvider;
@@ -97,7 +95,7 @@
this.psUtil = psUtil;
this.notifyResolver = notifyResolver;
this.projectCache = projectCache;
- this.urlFormatter = urlFormatter;
+ this.changeUtil = changeUtil;
}
@Override
@@ -115,14 +113,13 @@
String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message);
ensureCanEditCommitMessage(resource.getNotes());
- ChangeUtil.ensureChangeIdIsCorrect(
+ changeUtil.ensureChangeIdIsCorrect(
projectCache
.get(resource.getProject())
.orElseThrow(illegalState(resource.getProject()))
.is(BooleanProjectConfig.REQUIRE_CHANGE_ID),
resource.getChange().getKey().get(),
- sanitizedCommitMessage,
- urlFormatter.get());
+ sanitizedCommitMessage);
try (Repository repository = repositoryManager.openRepository(resource.getProject());
RevWalk revWalk = new RevWalk(repository);
diff --git a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
index d21bc9a..7f4b10f 100644
--- a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
+++ b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
@@ -31,6 +31,7 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.approval.ApprovalsUtil;
@@ -71,6 +72,7 @@
private final AccountResolver accountResolver;
private final ServiceUserClassifier serviceUserClassifier;
private final CommentsUtil commentsUtil;
+ private final DraftCommentsReader draftCommentsReader;
@Inject
ReplyAttentionSetUpdates(
@@ -80,7 +82,8 @@
ApprovalsUtil approvalsUtil,
AccountResolver accountResolver,
ServiceUserClassifier serviceUserClassifier,
- CommentsUtil commentsUtil) {
+ CommentsUtil commentsUtil,
+ DraftCommentsReader draftCommentsReader) {
this.permissionBackend = permissionBackend;
this.addToAttentionSetOpFactory = addToAttentionSetOpFactory;
this.removeFromAttentionSetOpFactory = removeFromAttentionSetOpFactory;
@@ -88,6 +91,7 @@
this.accountResolver = accountResolver;
this.serviceUserClassifier = serviceUserClassifier;
this.commentsUtil = commentsUtil;
+ this.draftCommentsReader = draftCommentsReader;
}
/** Adjusts the attention set but only based on the automatic rules. */
@@ -165,11 +169,13 @@
List<HumanComment> drafts = new ArrayList<>();
if (input.drafts == ReviewInput.DraftHandling.PUBLISH) {
drafts =
- commentsUtil.draftByPatchSetAuthor(
- changeNotes.getChange().currentPatchSetId(), currentUser.getAccountId(), changeNotes);
+ draftCommentsReader.getDraftsByPatchSetAndDraftAuthor(
+ changeNotes, changeNotes.getChange().currentPatchSetId(), currentUser.getAccountId());
}
if (input.drafts == ReviewInput.DraftHandling.PUBLISH_ALL_REVISIONS) {
- drafts = commentsUtil.draftByChangeAuthor(changeNotes, currentUser.getAccountId());
+ drafts =
+ draftCommentsReader.getDraftsByChangeAndDraftAuthor(
+ changeNotes, currentUser.getAccountId());
}
return Stream.concat(newComments.stream(), drafts.stream()).collect(toImmutableSet());
}
diff --git a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
index 50e774a..40b406c 100644
--- a/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
+++ b/java/com/google/gerrit/server/restapi/config/CheckConsistency.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountsConsistencyChecker;
import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
import com.google.gerrit.server.config.ConfigResource;
@@ -41,17 +42,20 @@
private final AccountsConsistencyChecker accountsConsistencyChecker;
private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
private final GroupsConsistencyChecker groupsConsistencyChecker;
+ private final AccountCache accountCache;
@Inject
CheckConsistency(
PermissionBackend permissionBackend,
AccountsConsistencyChecker accountsConsistencyChecker,
ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
- GroupsConsistencyChecker groupsChecker) {
+ GroupsConsistencyChecker groupsChecker,
+ AccountCache accountCache) {
this.permissionBackend = permissionBackend;
this.accountsConsistencyChecker = accountsConsistencyChecker;
this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
this.groupsConsistencyChecker = groupsChecker;
+ this.accountCache = accountCache;
}
@Override
@@ -73,7 +77,7 @@
}
if (input.checkAccountExternalIds != null) {
consistencyCheckInfo.checkAccountExternalIdsResult =
- new CheckAccountExternalIdsResultInfo(externalIdsConsistencyChecker.check());
+ new CheckAccountExternalIdsResultInfo(externalIdsConsistencyChecker.check(accountCache));
}
if (input.checkGroups != null) {
diff --git a/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java b/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
index 427ff84..3de05e9 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfigRestApiModule.java
@@ -29,22 +29,27 @@
DynamicMap.mapOf(binder(), CONFIG_KIND);
DynamicMap.mapOf(binder(), TASK_KIND);
DynamicMap.mapOf(binder(), TopMenuResource.TOP_MENU_KIND);
+
child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
- child(CONFIG_KIND, "tasks").to(TasksCollection.class);
- get(TASK_KIND).to(GetTask.class);
- delete(TASK_KIND).to(DeleteTask.class);
- child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
- get(CONFIG_KIND, "version").to(GetVersion.class);
- get(CONFIG_KIND, "info").to(GetServerInfo.class);
post(CONFIG_KIND, "check.consistency").to(CheckConsistency.class);
+ put(CONFIG_KIND, "email.confirm").to(ConfirmEmail.class);
post(CONFIG_KIND, "index.changes").to(IndexChanges.class);
- post(CONFIG_KIND, "reload").to(ReloadConfig.class);
+ get(CONFIG_KIND, "info").to(GetServerInfo.class);
get(CONFIG_KIND, "preferences").to(GetPreferences.class);
put(CONFIG_KIND, "preferences").to(SetPreferences.class);
get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);
put(CONFIG_KIND, "preferences.diff").to(SetDiffPreferences.class);
get(CONFIG_KIND, "preferences.edit").to(GetEditPreferences.class);
put(CONFIG_KIND, "preferences.edit").to(SetEditPreferences.class);
- put(CONFIG_KIND, "email.confirm").to(ConfirmEmail.class);
+ post(CONFIG_KIND, "reload").to(ReloadConfig.class);
+
+ child(CONFIG_KIND, "tasks").to(TasksCollection.class);
+ delete(TASK_KIND).to(DeleteTask.class);
+ get(TASK_KIND).to(GetTask.class);
+
+ child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
+ get(CONFIG_KIND, "version").to(GetVersion.class);
+
+ // The caches and summary REST endpoints are bound via RestCacheAdminModule.
}
}
diff --git a/java/com/google/gerrit/server/restapi/config/GetSummary.java b/java/com/google/gerrit/server/restapi/config/GetSummary.java
index 34cf550..faa3871 100644
--- a/java/com/google/gerrit/server/restapi/config/GetSummary.java
+++ b/java/com/google/gerrit/server/restapi/config/GetSummary.java
@@ -48,14 +48,6 @@
private final WorkQueue workQueue;
private final Path sitePath;
- @Option(name = "--gc", usage = "perform Java GC before retrieving memory stats")
- private boolean gc;
-
- public GetSummary setGc(boolean gc) {
- this.gc = gc;
- return this;
- }
-
@Option(name = "--jvm", usage = "include details about the JVM")
private boolean jvm;
@@ -72,12 +64,6 @@
@Override
public Response<SummaryInfo> apply(ConfigResource rsrc) {
- if (gc) {
- System.gc();
- System.runFinalization();
- System.gc();
- }
-
SummaryInfo summary = new SummaryInfo();
summary.taskSummary = getTaskSummary();
summary.memSummary = getMemSummary();
diff --git a/java/com/google/gerrit/server/restapi/config/RestCacheAdminModule.java b/java/com/google/gerrit/server/restapi/config/RestCacheAdminModule.java
index c929bc6..701d144d 100644
--- a/java/com/google/gerrit/server/restapi/config/RestCacheAdminModule.java
+++ b/java/com/google/gerrit/server/restapi/config/RestCacheAdminModule.java
@@ -25,10 +25,12 @@
@Override
protected void configure() {
DynamicMap.mapOf(binder(), CACHE_KIND);
+
child(CONFIG_KIND, "caches").to(CachesCollection.class);
postOnCollection(CACHE_KIND).to(PostCaches.class);
get(CACHE_KIND).to(GetCache.class);
post(CACHE_KIND, "flush").to(FlushCache.class);
+
get(CONFIG_KIND, "summary").to(GetSummary.class);
}
}
diff --git a/java/com/google/gerrit/server/restapi/group/GroupRestApiModule.java b/java/com/google/gerrit/server/restapi/group/GroupRestApiModule.java
index 8024862..f115374 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupRestApiModule.java
@@ -20,17 +20,12 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ServerInitiated;
-import com.google.gerrit.server.UserInitiated;
-import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.restapi.group.AddMembers.CreateMember;
import com.google.gerrit.server.restapi.group.AddMembers.UpdateMember;
import com.google.gerrit.server.restapi.group.AddSubgroups.CreateSubgroup;
import com.google.gerrit.server.restapi.group.AddSubgroups.UpdateSubgroup;
import com.google.gerrit.server.restapi.group.DeleteMembers.DeleteMember;
import com.google.gerrit.server.restapi.group.DeleteSubgroups.DeleteSubgroup;
-import com.google.inject.Provides;
public class GroupRestApiModule extends RestApiModule {
@@ -45,24 +40,23 @@
create(GROUP_KIND).to(CreateGroup.class);
get(GROUP_KIND).to(GetGroup.class);
put(GROUP_KIND).to(PutGroup.class);
- get(GROUP_KIND, "detail").to(GetDetail.class);
- post(GROUP_KIND, "index").to(Index.class);
- post(GROUP_KIND, "members").to(AddMembers.class);
- post(GROUP_KIND, "members.add").to(AddMembers.class);
- post(GROUP_KIND, "members.delete").to(DeleteMembers.class);
- post(GROUP_KIND, "groups").to(AddSubgroups.class);
- post(GROUP_KIND, "groups.add").to(AddSubgroups.class);
- post(GROUP_KIND, "groups.delete").to(DeleteSubgroups.class);
get(GROUP_KIND, "description").to(GetDescription.class);
put(GROUP_KIND, "description").to(PutDescription.class);
delete(GROUP_KIND, "description").to(PutDescription.class);
- get(GROUP_KIND, "name").to(GetName.class);
- put(GROUP_KIND, "name").to(PutName.class);
- get(GROUP_KIND, "owner").to(GetOwner.class);
- put(GROUP_KIND, "owner").to(PutOwner.class);
- get(GROUP_KIND, "options").to(GetOptions.class);
- put(GROUP_KIND, "options").to(PutOptions.class);
+ get(GROUP_KIND, "detail").to(GetDetail.class);
+ post(GROUP_KIND, "groups").to(AddSubgroups.class);
+
+ child(GROUP_KIND, "groups").to(SubgroupsCollection.class);
+ create(SUBGROUP_KIND).to(CreateSubgroup.class);
+ delete(SUBGROUP_KIND).to(DeleteSubgroup.class);
+ get(SUBGROUP_KIND).to(GetSubgroup.class);
+ put(SUBGROUP_KIND).to(UpdateSubgroup.class);
+
+ post(GROUP_KIND, "groups.add").to(AddSubgroups.class);
+ post(GROUP_KIND, "groups.delete").to(DeleteSubgroups.class);
+ post(GROUP_KIND, "index").to(Index.class);
get(GROUP_KIND, "log.audit").to(GetAuditLog.class);
+ post(GROUP_KIND, "members").to(AddMembers.class);
child(GROUP_KIND, "members").to(MembersCollection.class);
create(MEMBER_KIND).to(CreateMember.class);
@@ -70,25 +64,13 @@
put(MEMBER_KIND).to(UpdateMember.class);
delete(MEMBER_KIND).to(DeleteMember.class);
- child(GROUP_KIND, "groups").to(SubgroupsCollection.class);
- create(SUBGROUP_KIND).to(CreateSubgroup.class);
- get(SUBGROUP_KIND).to(GetSubgroup.class);
- put(SUBGROUP_KIND).to(UpdateSubgroup.class);
- delete(SUBGROUP_KIND).to(DeleteSubgroup.class);
-
- factory(GroupsUpdate.Factory.class);
- }
-
- @Provides
- @ServerInitiated
- GroupsUpdate provideServerInitiatedGroupsUpdate(GroupsUpdate.Factory groupsUpdateFactory) {
- return groupsUpdateFactory.createWithServerIdent();
- }
-
- @Provides
- @UserInitiated
- GroupsUpdate provideUserInitiatedGroupsUpdate(
- GroupsUpdate.Factory groupsUpdateFactory, IdentifiedUser currentUser) {
- return groupsUpdateFactory.create(currentUser);
+ post(GROUP_KIND, "members.add").to(AddMembers.class);
+ post(GROUP_KIND, "members.delete").to(DeleteMembers.class);
+ get(GROUP_KIND, "name").to(GetName.class);
+ put(GROUP_KIND, "name").to(PutName.class);
+ get(GROUP_KIND, "options").to(GetOptions.class);
+ put(GROUP_KIND, "options").to(PutOptions.class);
+ get(GROUP_KIND, "owner").to(GetOwner.class);
+ put(GROUP_KIND, "owner").to(PutOwner.class);
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/AbstractListProjects.java b/java/com/google/gerrit/server/restapi/project/AbstractListProjects.java
new file mode 100644
index 0000000..6467d81
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/AbstractListProjects.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2009 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.restapi.project;
+
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.json.OutputFormat;
+import org.kohsuke.args4j.Option;
+
+/**
+ * Base class for {@link ListProjects} implementations.
+ *
+ * <p>Defines the options that are supported by the list projects REST endpoint.
+ */
+public abstract class AbstractListProjects implements ListProjects {
+ @Override
+ @Option(name = "--format", usage = "(deprecated) output format")
+ public abstract void setFormat(OutputFormat fmt);
+
+ @Override
+ @Option(
+ name = "--show-branch",
+ aliases = {"-b"},
+ usage = "displays the sha of each project in the specified branch")
+ public abstract void addShowBranch(String branch);
+
+ @Override
+ @Option(
+ name = "--tree",
+ aliases = {"-t"},
+ usage =
+ "displays project inheritance in a tree-like format\n"
+ + "this option does not work together with the show-branch option")
+ public abstract void setShowTree(boolean showTree);
+
+ @Override
+ @Option(name = "--type", usage = "type of project")
+ public abstract void setFilterType(FilterType type);
+
+ @Override
+ @Option(
+ name = "--description",
+ aliases = {"-d"},
+ usage = "include description of project in list")
+ public abstract void setShowDescription(boolean showDescription);
+
+ @Override
+ @Option(name = "--all", usage = "display all projects that are accessible by the calling user")
+ public abstract void setAll(boolean all);
+
+ @Override
+ @Option(
+ name = "--state",
+ aliases = {"-s"},
+ usage = "filter by project state")
+ public abstract void setState(com.google.gerrit.extensions.client.ProjectState state);
+
+ @Override
+ @Option(
+ name = "--limit",
+ aliases = {"-n"},
+ metaVar = "CNT",
+ usage = "maximum number of projects to list")
+ public abstract void setLimit(int limit);
+
+ @Override
+ @Option(
+ name = "--start",
+ aliases = {"-S"},
+ metaVar = "CNT",
+ usage = "number of projects to skip")
+ public abstract void setStart(int start);
+
+ @Override
+ @Option(
+ name = "--prefix",
+ aliases = {"-p"},
+ metaVar = "PREFIX",
+ usage = "match project prefix")
+ public abstract void setMatchPrefix(String matchPrefix);
+
+ @Override
+ @Option(
+ name = "--match",
+ aliases = {"-m"},
+ metaVar = "MATCH",
+ usage = "match project substring")
+ public abstract void setMatchSubstring(String matchSubstring);
+
+ @Override
+ @Option(name = "-r", metaVar = "REGEX", usage = "match project regex")
+ public abstract void setMatchRegex(String matchRegex);
+
+ @Override
+ @Option(
+ name = "--has-acl-for",
+ metaVar = "GROUP",
+ usage = "displays only projects on which access rights for this group are directly assigned")
+ public abstract void setGroupUuid(AccountGroup.UUID groupUuid);
+
+ @Override
+ public Response<Object> apply(TopLevelResource resource) throws Exception {
+ return Response.ok(apply());
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index cfdadd9..04819d8 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -227,7 +227,7 @@
return branch;
}
- static class ValidBranchListener implements ProjectCreationValidationListener {
+ public static class ValidBranchListener implements ProjectCreationValidationListener {
@Override
public void validateNewProject(CreateProjectArgs args) throws ValidationException {
for (String branch : args.branch) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index 1f40de6..3ce8708 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -14,90 +14,25 @@
package com.google.gerrit.server.restapi.project;
-import static com.google.common.base.Strings.emptyToNull;
-import static com.google.common.base.Strings.isNullOrEmpty;
-import static com.google.common.collect.Ordering.natural;
-import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSortedMap;
-import com.google.common.collect.Iterables;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
-import com.google.gerrit.entities.GroupReference;
-import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.NoSuchGroupException;
-import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.common.WebLinkInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.BinaryResult;
-import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.WebLinks;
-import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.group.GroupResolver;
-import com.google.gerrit.server.ioutil.RegexListSearcher;
-import com.google.gerrit.server.ioutil.StringUtil;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.util.TreeFormatter;
-import com.google.gson.reflect.TypeToken;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.BufferedWriter;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.NavigableSet;
-import java.util.Optional;
import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-import org.kohsuke.args4j.Option;
/**
* List projects visible to the calling user.
*
* <p>Implement {@code GET /projects/}, without a {@code query=} parameter.
*/
-public class ListProjects implements RestReadView<TopLevelResource> {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
+public interface ListProjects extends RestReadView<TopLevelResource> {
public enum FilterType {
CODE {
@Override
@@ -141,598 +76,36 @@
abstract boolean useMatch();
}
- private final CurrentUser currentUser;
- private final ProjectCache projectCache;
- private final GroupResolver groupResolver;
- private final GroupControl.Factory groupControlFactory;
- private final GitRepositoryManager repoManager;
- private final PermissionBackend permissionBackend;
- private final ProjectNode.Factory projectNodeFactory;
- private final WebLinks webLinks;
+ void setFormat(OutputFormat fmt);
- @Deprecated
- @Option(name = "--format", usage = "(deprecated) output format")
- private OutputFormat format = OutputFormat.TEXT;
+ void addShowBranch(String branch);
- @Option(
- name = "--show-branch",
- aliases = {"-b"},
- usage = "displays the sha of each project in the specified branch")
- public void addShowBranch(String branch) {
- showBranch.add(branch);
- }
+ void setShowTree(boolean showTree);
- @Option(
- name = "--tree",
- aliases = {"-t"},
- usage =
- "displays project inheritance in a tree-like format\n"
- + "this option does not work together with the show-branch option")
- public void setShowTree(boolean showTree) {
- this.showTree = showTree;
- }
+ void setFilterType(FilterType type);
- @Option(name = "--type", usage = "type of project")
- public void setFilterType(FilterType type) {
- this.type = type;
- }
+ void setShowDescription(boolean showDescription);
- @Option(
- name = "--description",
- aliases = {"-d"},
- usage = "include description of project in list")
- public void setShowDescription(boolean showDescription) {
- this.showDescription = showDescription;
- }
+ void setAll(boolean all);
- @Option(name = "--all", usage = "display all projects that are accessible by the calling user")
- public void setAll(boolean all) {
- this.all = all;
- }
+ void setState(com.google.gerrit.extensions.client.ProjectState state);
- @Option(
- name = "--state",
- aliases = {"-s"},
- usage = "filter by project state")
- public void setState(com.google.gerrit.extensions.client.ProjectState state) {
- this.state = state;
- }
+ void setLimit(int limit);
- @Option(
- name = "--limit",
- aliases = {"-n"},
- metaVar = "CNT",
- usage = "maximum number of projects to list")
- public void setLimit(int limit) {
- this.limit = limit;
- }
+ void setStart(int start);
- @Option(
- name = "--start",
- aliases = {"-S"},
- metaVar = "CNT",
- usage = "number of projects to skip")
- public void setStart(int start) {
- this.start = start;
- }
+ void setMatchPrefix(String matchPrefix);
- @Option(
- name = "--prefix",
- aliases = {"-p"},
- metaVar = "PREFIX",
- usage = "match project prefix")
- public void setMatchPrefix(String matchPrefix) {
- this.matchPrefix = matchPrefix;
- }
+ void setMatchSubstring(String matchSubstring);
- @Option(
- name = "--match",
- aliases = {"-m"},
- metaVar = "MATCH",
- usage = "match project substring")
- public void setMatchSubstring(String matchSubstring) {
- this.matchSubstring = matchSubstring;
- }
+ void setMatchRegex(String matchRegex);
- @Option(name = "-r", metaVar = "REGEX", usage = "match project regex")
- public void setMatchRegex(String matchRegex) {
- this.matchRegex = matchRegex;
- }
-
- @Option(
- name = "--has-acl-for",
- metaVar = "GROUP",
- usage = "displays only projects on which access rights for this group are directly assigned")
- public void setGroupUuid(AccountGroup.UUID groupUuid) {
- this.groupUuid = groupUuid;
- }
-
- private final List<String> showBranch = new ArrayList<>();
- private boolean showTree;
- private FilterType type = FilterType.ALL;
- private boolean showDescription;
- private boolean all;
- private com.google.gerrit.extensions.client.ProjectState state;
- private int limit;
- private int start;
- private String matchPrefix;
- private String matchSubstring;
- private String matchRegex;
- private AccountGroup.UUID groupUuid;
- private final Provider<QueryProjects> queryProjectsProvider;
- private final boolean listProjectsFromIndex;
-
- @Inject
- protected ListProjects(
- CurrentUser currentUser,
- ProjectCache projectCache,
- GroupResolver groupResolver,
- GroupControl.Factory groupControlFactory,
- GitRepositoryManager repoManager,
- PermissionBackend permissionBackend,
- ProjectNode.Factory projectNodeFactory,
- WebLinks webLinks,
- Provider<QueryProjects> queryProjectsProvider,
- @GerritServerConfig Config config) {
- this.currentUser = currentUser;
- this.projectCache = projectCache;
- this.groupResolver = groupResolver;
- this.groupControlFactory = groupControlFactory;
- this.repoManager = repoManager;
- this.permissionBackend = permissionBackend;
- this.projectNodeFactory = projectNodeFactory;
- this.webLinks = webLinks;
- this.queryProjectsProvider = queryProjectsProvider;
- this.listProjectsFromIndex = config.getBoolean("gerrit", "listProjectsFromIndex", false);
- }
-
- public List<String> getShowBranch() {
- return showBranch;
- }
-
- public boolean isShowTree() {
- return showTree;
- }
-
- public boolean isShowDescription() {
- return showDescription;
- }
-
- public OutputFormat getFormat() {
- return format;
- }
-
- public ListProjects setFormat(OutputFormat fmt) {
- format = fmt;
- return this;
- }
+ void setGroupUuid(AccountGroup.UUID groupUuid);
@Override
- public Response<Object> apply(TopLevelResource resource)
- throws BadRequestException, PermissionBackendException {
- if (format == OutputFormat.TEXT) {
- ByteArrayOutputStream buf = new ByteArrayOutputStream();
- displayToStream(buf);
- return Response.ok(
- BinaryResult.create(buf.toByteArray())
- .setContentType("text/plain")
- .setCharacterEncoding(UTF_8));
- }
+ default Response<Object> apply(TopLevelResource resource) throws Exception {
return Response.ok(apply());
}
- public SortedMap<String, ProjectInfo> apply()
- throws BadRequestException, PermissionBackendException {
- Optional<String> projectQuery = expressAsProjectsQuery();
- if (projectQuery.isPresent()) {
- return applyAsQuery(projectQuery.get());
- }
-
- format = OutputFormat.JSON;
- return display(null);
- }
-
- private Optional<String> expressAsProjectsQuery() {
- return listProjectsFromIndex
- && !all
- && state != HIDDEN
- && isNullOrEmpty(matchPrefix)
- && isNullOrEmpty(matchRegex)
- && isNullOrEmpty(
- matchSubstring) // TODO: see https://issues.gerritcodereview.com/issues/40010295
- && type == FilterType.ALL
- && showBranch.isEmpty()
- && !showTree
- ? Optional.of(stateToQuery())
- : Optional.empty();
- }
-
- private String stateToQuery() {
- List<String> queries = new ArrayList<>();
- if (state == null) {
- queries.add("(state:active OR state:read-only)");
- } else {
- queries.add(String.format("(state:%s)", state.name()));
- }
-
- return Joiner.on(" AND ").join(queries);
- }
-
- private SortedMap<String, ProjectInfo> applyAsQuery(String query) throws BadRequestException {
- try {
- return queryProjectsProvider.get().withQuery(query).withStart(start).withLimit(limit).apply()
- .stream()
- .collect(
- ImmutableSortedMap.toImmutableSortedMap(
- natural(), p -> p.name, p -> showDescription ? p : nullifyDescription(p)));
- } catch (StorageException | MethodNotAllowedException e) {
- logger.atWarning().withCause(e).log(
- "Internal error while processing the query '%s' request", query);
- throw new BadRequestException("Internal error while processing the query request");
- }
- }
-
- private ProjectInfo nullifyDescription(ProjectInfo p) {
- p.description = null;
- return p;
- }
-
- private void printQueryResults(String query, PrintWriter out) throws BadRequestException {
- try {
- if (format.isJson()) {
- format.newGson().toJson(applyAsQuery(query), out);
- } else {
- newProjectsNamesStream(query).forEach(out::println);
- }
- out.flush();
- } catch (StorageException | MethodNotAllowedException e) {
- logger.atWarning().withCause(e).log(
- "Internal error while processing the query '%s' request", query);
- throw new BadRequestException("Internal error while processing the query request");
- }
- }
-
- private Stream<String> newProjectsNamesStream(String query)
- throws MethodNotAllowedException, BadRequestException {
- Stream<String> projects =
- queryProjectsProvider.get().withQuery(query).apply().stream().map(p -> p.name).skip(start);
- if (limit > 0) {
- projects = projects.limit(limit);
- }
-
- return projects;
- }
-
- public void displayToStream(OutputStream displayOutputStream)
- throws BadRequestException, PermissionBackendException {
- PrintWriter stdout =
- new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
- Optional<String> projectsQuery = expressAsProjectsQuery();
-
- if (projectsQuery.isPresent()) {
- printQueryResults(projectsQuery.get(), stdout);
- } else {
- display(stdout);
- }
- }
-
- @Nullable
- public SortedMap<String, ProjectInfo> display(@Nullable PrintWriter stdout)
- throws BadRequestException, PermissionBackendException {
- if (all && state != null) {
- throw new BadRequestException("'all' and 'state' may not be used together");
- }
- if (!isGroupVisible()) {
- return Collections.emptySortedMap();
- }
-
- int foundIndex = 0;
- int found = 0;
- TreeMap<String, ProjectInfo> output = new TreeMap<>();
- Map<String, String> hiddenNames = new HashMap<>();
- Map<Project.NameKey, Boolean> accessibleParents = new HashMap<>();
- PermissionBackend.WithUser perm = permissionBackend.user(currentUser);
- final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
- try {
- Iterator<ProjectState> projectStatesIt = filter(perm).iterator();
- while (projectStatesIt.hasNext()) {
- ProjectState e = projectStatesIt.next();
- Project.NameKey projectName = e.getNameKey();
- if (e.getProject().getState() == HIDDEN && !all && state != HIDDEN) {
- // If we can't get it from the cache, pretend it's not present.
- // If all wasn't selected, and it's HIDDEN, pretend it's not present.
- // If state HIDDEN wasn't selected, and it's HIDDEN, pretend it's not present.
- continue;
- }
-
- if (state != null && e.getProject().getState() != state) {
- continue;
- }
-
- if (groupUuid != null
- && !e.getLocalGroups()
- .contains(GroupReference.forGroup(groupResolver.parseId(groupUuid.get())))) {
- continue;
- }
-
- if (showTree && !format.isJson()) {
- treeMap.put(projectName, projectNodeFactory.create(e.getProject(), true));
- continue;
- }
-
- if (foundIndex++ < start) {
- continue;
- }
- if (limit > 0 && ++found > limit) {
- break;
- }
-
- ProjectInfo info = new ProjectInfo();
- info.name = projectName.get();
- if (showTree && format.isJson()) {
- addParentProjectInfo(hiddenNames, accessibleParents, perm, e, info);
- }
-
- if (showDescription) {
- info.description = emptyToNull(e.getProject().getDescription());
- }
- info.state = e.getProject().getState();
-
- try {
- if (!showBranch.isEmpty()) {
- try (Repository git = repoManager.openRepository(projectName)) {
- if (!type.matches(git)) {
- continue;
- }
-
- List<Ref> refs = retrieveBranchRefs(e, git);
- if (!hasValidRef(refs)) {
- continue;
- }
-
- addProjectBranchesInfo(info, refs);
- }
- } else if (!showTree && type.useMatch()) {
- try (Repository git = repoManager.openRepository(projectName)) {
- if (!type.matches(git)) {
- continue;
- }
- }
- }
- } catch (RepositoryNotFoundException err) {
- // If the Git repository is gone, the project doesn't actually exist anymore.
- continue;
- } catch (IOException err) {
- logger.atWarning().withCause(err).log("Unexpected error reading %s", projectName);
- continue;
- }
-
- ImmutableList<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get());
- info.webLinks = links.isEmpty() ? null : links;
-
- if (stdout == null || format.isJson()) {
- output.put(info.name, info);
- continue;
- }
-
- if (!showBranch.isEmpty()) {
- printProjectBranches(stdout, info);
- }
- stdout.print(info.name);
-
- if (info.description != null) {
- // We still want to list every project as one-liners, hence escaping \n.
- stdout.print(" - " + StringUtil.escapeString(info.description));
- }
- stdout.print('\n');
- }
-
- for (ProjectInfo info : output.values()) {
- info.id = Url.encode(info.name);
- info.name = null;
- }
- if (stdout == null) {
- return output;
- } else if (format.isJson()) {
- format
- .newGson()
- .toJson(output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
- stdout.print('\n');
- } else if (showTree && treeMap.size() > 0) {
- printProjectTree(stdout, treeMap);
- }
- return null;
- } finally {
- if (stdout != null) {
- stdout.flush();
- }
- }
- }
-
- private boolean isGroupVisible() {
- try {
- return groupUuid == null || groupControlFactory.controlFor(groupUuid).isVisible();
- } catch (NoSuchGroupException ex) {
- return false;
- }
- }
-
- private void printProjectBranches(PrintWriter stdout, ProjectInfo info) {
- for (String name : showBranch) {
- String ref = info.branches != null ? info.branches.get(name) : null;
- if (ref == null) {
- // Print stub (forty '-' symbols)
- ref = "----------------------------------------";
- }
- stdout.print(ref);
- stdout.print(' ');
- }
- }
-
- private void addProjectBranchesInfo(ProjectInfo info, List<Ref> refs) {
- for (int i = 0; i < showBranch.size(); i++) {
- Ref ref = refs.get(i);
- if (ref != null && ref.getObjectId() != null) {
- if (info.branches == null) {
- info.branches = new LinkedHashMap<>();
- }
- info.branches.put(showBranch.get(i), ref.getObjectId().name());
- }
- }
- }
-
- private List<Ref> retrieveBranchRefs(ProjectState e, Repository git) {
- if (!e.statePermitsRead()) {
- return ImmutableList.of();
- }
-
- return getBranchRefs(e.getNameKey(), git);
- }
-
- private void addParentProjectInfo(
- Map<String, String> hiddenNames,
- Map<Project.NameKey, Boolean> accessibleParents,
- PermissionBackend.WithUser perm,
- ProjectState e,
- ProjectInfo info)
- throws PermissionBackendException {
- ProjectState parent = Iterables.getFirst(e.parents(), null);
- if (parent != null) {
- if (isParentAccessible(accessibleParents, perm, parent)) {
- info.parent = parent.getName();
- } else {
- info.parent = hiddenNames.get(parent.getName());
- if (info.parent == null) {
- info.parent = "?-" + (hiddenNames.size() + 1);
- hiddenNames.put(parent.getName(), info.parent);
- }
- }
- }
- }
-
- private Stream<ProjectState> filter(PermissionBackend.WithUser perm) throws BadRequestException {
- return StreamSupport.stream(scan().spliterator(), false)
- .map(projectCache::get)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .filter(p -> permissionCheck(p, perm));
- }
-
- private boolean permissionCheck(ProjectState state, PermissionBackend.WithUser perm) {
- // Hidden projects(permitsRead = false) should only be accessible by the project owners.
- // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
- // be allowed for other users). Allowing project owners to access here will help them to view
- // and update the config of hidden projects easily.
- return perm.project(state.getNameKey())
- .testOrFalse(
- state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG);
- }
-
- private boolean isParentAccessible(
- Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState state)
- throws PermissionBackendException {
- Project.NameKey name = state.getNameKey();
- Boolean b = checked.get(name);
- if (b == null) {
- try {
- // Hidden projects(permitsRead = false) should only be accessible by the project owners.
- // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
- // be allowed for other users). Allowing project owners to access here will help them to
- // view
- // and update the config of hidden projects easily.
- ProjectPermission permissionToCheck =
- state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
- perm.project(name).check(permissionToCheck);
- b = true;
- } catch (AuthException denied) {
- b = false;
- }
- checked.put(name, b);
- }
- return b;
- }
-
- private Stream<Project.NameKey> scan() throws BadRequestException {
- if (matchPrefix != null) {
- checkMatchOptions(matchSubstring == null && matchRegex == null);
- return projectCache.byName(matchPrefix).stream();
- } else if (matchSubstring != null) {
- checkMatchOptions(matchPrefix == null && matchRegex == null);
- return projectCache.all().stream()
- .filter(
- p -> p.get().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US)));
- } else if (matchRegex != null) {
- checkMatchOptions(matchPrefix == null && matchSubstring == null);
- RegexListSearcher<Project.NameKey> searcher;
- try {
- searcher = new RegexListSearcher<>(matchRegex, Project.NameKey::get);
- } catch (IllegalArgumentException e) {
- throw new BadRequestException(e.getMessage());
- }
- return searcher.search(projectCache.all().asList());
- } else {
- return projectCache.all().stream();
- }
- }
-
- private static void checkMatchOptions(boolean cond) throws BadRequestException {
- if (!cond) {
- throw new BadRequestException("specify exactly one of p/m/r");
- }
- }
-
- private void printProjectTree(
- final PrintWriter stdout, TreeMap<Project.NameKey, ProjectNode> treeMap) {
- final NavigableSet<ProjectNode> sortedNodes = new TreeSet<>();
-
- // Builds the inheritance tree using a list.
- //
- for (ProjectNode key : treeMap.values()) {
- if (key.isAllProjects()) {
- sortedNodes.add(key);
- continue;
- }
-
- ProjectNode node = treeMap.get(key.getParentName());
- if (node != null) {
- node.addChild(key);
- } else {
- sortedNodes.add(key);
- }
- }
-
- final TreeFormatter treeFormatter = new TreeFormatter(stdout);
- treeFormatter.printTree(sortedNodes);
- stdout.flush();
- }
-
- private List<Ref> getBranchRefs(Project.NameKey projectName, Repository git) {
- Ref[] result = new Ref[showBranch.size()];
- try {
- PermissionBackend.ForProject perm = permissionBackend.user(currentUser).project(projectName);
- for (int i = 0; i < showBranch.size(); i++) {
- Ref ref = git.findRef(showBranch.get(i));
- if (ref != null && ref.getObjectId() != null) {
- try {
- perm.ref(ref.getLeaf().getName()).check(RefPermission.READ);
- result[i] = ref;
- } catch (AuthException e) {
- continue;
- }
- }
- }
- } catch (IOException | PermissionBackendException e) {
- // Fall through and return what is available.
- }
- return Arrays.asList(result);
- }
-
- private static boolean hasValidRef(List<Ref> refs) {
- for (Ref ref : refs) {
- if (ref != null) {
- return true;
- }
- }
- return false;
- }
+ SortedMap<String, ProjectInfo> apply() throws Exception;
}
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
new file mode 100644
index 0000000..88aec1a
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
@@ -0,0 +1,686 @@
+// Copyright (C) 2009 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.restapi.project;
+
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.collect.Ordering.natural;
+import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.index.project.ProjectField;
+import com.google.gerrit.index.project.ProjectIndexCollection;
+import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.WebLinks;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.group.GroupResolver;
+import com.google.gerrit.server.ioutil.RegexListSearcher;
+import com.google.gerrit.server.ioutil.StringUtil;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.TreeFormatter;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Optional;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * List projects visible to the calling user.
+ *
+ * <p>Implement {@code GET /projects/}, without a {@code query=} parameter.
+ */
+public class ListProjectsImpl extends AbstractListProjects {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final CurrentUser currentUser;
+ private final ProjectCache projectCache;
+ private final GroupResolver groupResolver;
+ private final GroupControl.Factory groupControlFactory;
+ private final GitRepositoryManager repoManager;
+ private final PermissionBackend permissionBackend;
+ private final ProjectNode.Factory projectNodeFactory;
+ private final WebLinks webLinks;
+
+ @Override
+ public void setFormat(OutputFormat fmt) {
+ format = fmt;
+ }
+
+ @Override
+ public void addShowBranch(String branch) {
+ showBranch.add(branch);
+ }
+
+ @Override
+ public void setShowTree(boolean showTree) {
+ this.showTree = showTree;
+ }
+
+ @Override
+ public void setFilterType(FilterType type) {
+ this.type = type;
+ }
+
+ @Override
+ public void setShowDescription(boolean showDescription) {
+ this.showDescription = showDescription;
+ }
+
+ @Override
+ public void setAll(boolean all) {
+ this.all = all;
+ }
+
+ @Override
+ public void setState(com.google.gerrit.extensions.client.ProjectState state) {
+ this.state = state;
+ }
+
+ @Override
+ public void setLimit(int limit) {
+ this.limit = limit;
+ }
+
+ @Override
+ public void setStart(int start) {
+ this.start = start;
+ }
+
+ @Override
+ public void setMatchPrefix(String matchPrefix) {
+ this.matchPrefix = matchPrefix;
+ }
+
+ @Override
+ public void setMatchSubstring(String matchSubstring) {
+ this.matchSubstring = matchSubstring;
+ }
+
+ @Override
+ public void setMatchRegex(String matchRegex) {
+ this.matchRegex = matchRegex;
+ }
+
+ @Override
+ public void setGroupUuid(AccountGroup.UUID groupUuid) {
+ this.groupUuid = groupUuid;
+ }
+
+ @Deprecated private OutputFormat format = OutputFormat.TEXT;
+ private final List<String> showBranch = new ArrayList<>();
+ private boolean showTree;
+ private FilterType type = FilterType.ALL;
+ private boolean showDescription;
+ private boolean all;
+ private com.google.gerrit.extensions.client.ProjectState state;
+ private int limit;
+ private int start;
+ private String matchPrefix;
+ private String matchSubstring;
+ private String matchRegex;
+ private AccountGroup.UUID groupUuid;
+ private final Provider<QueryProjects> queryProjectsProvider;
+ private final boolean listProjectsFromIndex;
+ private final ProjectIndexCollection projectIndexes;
+
+ @Inject
+ protected ListProjectsImpl(
+ CurrentUser currentUser,
+ ProjectCache projectCache,
+ GroupResolver groupResolver,
+ GroupControl.Factory groupControlFactory,
+ GitRepositoryManager repoManager,
+ PermissionBackend permissionBackend,
+ ProjectNode.Factory projectNodeFactory,
+ WebLinks webLinks,
+ Provider<QueryProjects> queryProjectsProvider,
+ @GerritServerConfig Config config,
+ ProjectIndexCollection projectIndexes) {
+ this.currentUser = currentUser;
+ this.projectCache = projectCache;
+ this.groupResolver = groupResolver;
+ this.groupControlFactory = groupControlFactory;
+ this.repoManager = repoManager;
+ this.permissionBackend = permissionBackend;
+ this.projectNodeFactory = projectNodeFactory;
+ this.webLinks = webLinks;
+ this.queryProjectsProvider = queryProjectsProvider;
+ this.listProjectsFromIndex = config.getBoolean("gerrit", "listProjectsFromIndex", false);
+ this.projectIndexes = projectIndexes;
+ }
+
+ public List<String> getShowBranch() {
+ return showBranch;
+ }
+
+ public boolean isShowTree() {
+ return showTree;
+ }
+
+ public boolean isShowDescription() {
+ return showDescription;
+ }
+
+ public OutputFormat getFormat() {
+ return format;
+ }
+
+ @Override
+ public Response<Object> apply(TopLevelResource resource)
+ throws BadRequestException, PermissionBackendException {
+ if (format == OutputFormat.TEXT) {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ displayToStream(buf);
+ return Response.ok(
+ BinaryResult.create(buf.toByteArray())
+ .setContentType("text/plain")
+ .setCharacterEncoding(UTF_8));
+ }
+ return Response.ok(apply());
+ }
+
+ @Override
+ public SortedMap<String, ProjectInfo> apply()
+ throws BadRequestException, PermissionBackendException {
+ Optional<String> projectQuery = expressAsProjectsQuery();
+ if (projectQuery.isPresent()) {
+ return applyAsQuery(projectQuery.get());
+ }
+
+ format = OutputFormat.JSON;
+ return display(null);
+ }
+
+ private Optional<String> expressAsProjectsQuery() throws BadRequestException {
+ return listProjectsFromIndex
+ && !all
+ && state != HIDDEN
+ && (isNullOrEmpty(matchPrefix)
+ || projectIndexes
+ .getSearchIndex()
+ .getSchema()
+ .hasField(ProjectField.PREFIX_NAME_SPEC))
+ && isNullOrEmpty(matchRegex)
+ && type == FilterType.ALL
+ && showBranch.isEmpty()
+ && !showTree
+ ? Optional.of(toQuery())
+ : Optional.empty();
+ }
+
+ private String toQuery() throws BadRequestException {
+ // QueryProjects supports specifying matchPrefix and matchSubstring at the same time, but to
+ // keep the behavior consistent regardless of whether 'gerrit.listProjectsFromIndex' is true or
+ // false, disallow specifying both at the same time here. This way
+ // 'gerrit.listProjectsFromIndex' can be troggled without breaking any caller.
+ if (matchPrefix != null) {
+ checkMatchOptions(matchSubstring == null);
+ } else if (matchSubstring != null) {
+ checkMatchOptions(matchPrefix == null);
+ }
+
+ List<String> queries = new ArrayList<>();
+
+ if (state != null) {
+ queries.add(String.format("(state:%s)", state.name()));
+ }
+ if (!isNullOrEmpty(matchPrefix)) {
+ queries.add(String.format("prefix:%s", matchPrefix));
+ }
+ if (!isNullOrEmpty(matchSubstring)) {
+ queries.add(String.format("substring:%s", matchSubstring));
+ }
+
+ return queries.isEmpty() ? "" : Joiner.on(" AND ").join(queries);
+ }
+
+ private SortedMap<String, ProjectInfo> applyAsQuery(String query) throws BadRequestException {
+ try {
+ return queryProjectsProvider.get().withQuery(query).withStart(start).withLimit(limit).apply()
+ .stream()
+ .collect(
+ ImmutableSortedMap.toImmutableSortedMap(
+ natural(), p -> p.name, p -> showDescription ? p : nullifyDescription(p)));
+ } catch (StorageException | MethodNotAllowedException e) {
+ logger.atWarning().withCause(e).log(
+ "Internal error while processing the query '%s' request", query);
+ throw new BadRequestException("Internal error while processing the query request");
+ }
+ }
+
+ private ProjectInfo nullifyDescription(ProjectInfo p) {
+ p.description = null;
+ return p;
+ }
+
+ private void printQueryResults(String query, PrintWriter out) throws BadRequestException {
+ try {
+ if (format.isJson()) {
+ format.newGson().toJson(applyAsQuery(query), out);
+ } else {
+ newProjectsNamesStream(query).forEach(out::println);
+ }
+ out.flush();
+ } catch (StorageException | MethodNotAllowedException e) {
+ logger.atWarning().withCause(e).log(
+ "Internal error while processing the query '%s' request", query);
+ throw new BadRequestException("Internal error while processing the query request");
+ }
+ }
+
+ private Stream<String> newProjectsNamesStream(String query)
+ throws MethodNotAllowedException, BadRequestException {
+ Stream<String> projects =
+ queryProjectsProvider.get().withQuery(query).apply().stream().map(p -> p.name).skip(start);
+ if (limit > 0) {
+ projects = projects.limit(limit);
+ }
+
+ return projects;
+ }
+
+ public void displayToStream(OutputStream displayOutputStream)
+ throws BadRequestException, PermissionBackendException {
+ PrintWriter stdout =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
+ Optional<String> projectsQuery = expressAsProjectsQuery();
+
+ if (projectsQuery.isPresent()) {
+ printQueryResults(projectsQuery.get(), stdout);
+ } else {
+ display(stdout);
+ }
+ }
+
+ @CanIgnoreReturnValue
+ @Nullable
+ public SortedMap<String, ProjectInfo> display(@Nullable PrintWriter stdout)
+ throws BadRequestException, PermissionBackendException {
+ if (all && state != null) {
+ throw new BadRequestException("'all' and 'state' may not be used together");
+ }
+ if (!isGroupVisible()) {
+ return Collections.emptySortedMap();
+ }
+
+ int foundIndex = 0;
+ int found = 0;
+ TreeMap<String, ProjectInfo> output = new TreeMap<>();
+ Map<String, String> hiddenNames = new HashMap<>();
+ Map<Project.NameKey, Boolean> accessibleParents = new HashMap<>();
+ PermissionBackend.WithUser perm = permissionBackend.user(currentUser);
+ final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
+ ProjectInfo lastInfo = null;
+ try {
+ Iterator<ProjectState> projectStatesIt = filter(perm).iterator();
+ while (projectStatesIt.hasNext()) {
+ ProjectState e = projectStatesIt.next();
+ Project.NameKey projectName = e.getNameKey();
+ if (e.getProject().getState() == HIDDEN && !all && state != HIDDEN) {
+ // If we can't get it from the cache, pretend it's not present.
+ // If all wasn't selected, and it's HIDDEN, pretend it's not present.
+ // If state HIDDEN wasn't selected, and it's HIDDEN, pretend it's not present.
+ continue;
+ }
+
+ if (state != null && e.getProject().getState() != state) {
+ continue;
+ }
+
+ if (groupUuid != null
+ && !e.getLocalGroups()
+ .contains(GroupReference.forGroup(groupResolver.parseId(groupUuid.get())))) {
+ continue;
+ }
+
+ if (showTree && !format.isJson()) {
+ treeMap.put(projectName, projectNodeFactory.create(e.getProject(), true));
+ continue;
+ }
+
+ if (foundIndex++ < start) {
+ continue;
+ }
+ if (limit > 0 && ++found > limit) {
+ if (lastInfo != null) {
+ lastInfo._moreProjects = true;
+ }
+ break;
+ }
+
+ ProjectInfo info = new ProjectInfo();
+ lastInfo = info;
+
+ info.name = projectName.get();
+ if (showTree && format.isJson()) {
+ addParentProjectInfo(hiddenNames, accessibleParents, perm, e, info);
+ }
+
+ if (showDescription) {
+ info.description = emptyToNull(e.getProject().getDescription());
+ }
+ info.state = e.getProject().getState();
+
+ try {
+ if (!showBranch.isEmpty()) {
+ try (Repository git = repoManager.openRepository(projectName)) {
+ if (!type.matches(git)) {
+ continue;
+ }
+
+ List<Ref> refs = retrieveBranchRefs(e, git);
+ if (!hasValidRef(refs)) {
+ continue;
+ }
+
+ addProjectBranchesInfo(info, refs);
+ }
+ } else if (!showTree && type.useMatch()) {
+ try (Repository git = repoManager.openRepository(projectName)) {
+ if (!type.matches(git)) {
+ continue;
+ }
+ }
+ }
+ } catch (RepositoryNotFoundException err) {
+ // If the Git repository is gone, the project doesn't actually exist anymore.
+ continue;
+ } catch (IOException err) {
+ logger.atWarning().withCause(err).log("Unexpected error reading %s", projectName);
+ continue;
+ }
+
+ ImmutableList<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get());
+ info.webLinks = links.isEmpty() ? null : links;
+
+ if (stdout == null || format.isJson()) {
+ output.put(info.name, info);
+ continue;
+ }
+
+ if (!showBranch.isEmpty()) {
+ printProjectBranches(stdout, info);
+ }
+ stdout.print(info.name);
+
+ if (info.description != null) {
+ // We still want to list every project as one-liners, hence escaping \n.
+ stdout.print(" - " + StringUtil.escapeString(info.description));
+ }
+ stdout.print('\n');
+ }
+
+ for (ProjectInfo info : output.values()) {
+ info.id = Url.encode(info.name);
+ info.name = null;
+ }
+ if (stdout == null) {
+ return output;
+ } else if (format.isJson()) {
+ format
+ .newGson()
+ .toJson(output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
+ stdout.print('\n');
+ } else if (showTree && treeMap.size() > 0) {
+ printProjectTree(stdout, treeMap);
+ }
+ return null;
+ } finally {
+ if (stdout != null) {
+ stdout.flush();
+ }
+ }
+ }
+
+ private boolean isGroupVisible() {
+ try {
+ return groupUuid == null || groupControlFactory.controlFor(groupUuid).isVisible();
+ } catch (NoSuchGroupException ex) {
+ return false;
+ }
+ }
+
+ private void printProjectBranches(PrintWriter stdout, ProjectInfo info) {
+ for (String name : showBranch) {
+ String ref = info.branches != null ? info.branches.get(name) : null;
+ if (ref == null) {
+ // Print stub (forty '-' symbols)
+ ref = "----------------------------------------";
+ }
+ stdout.print(ref);
+ stdout.print(' ');
+ }
+ }
+
+ private void addProjectBranchesInfo(ProjectInfo info, List<Ref> refs) {
+ for (int i = 0; i < showBranch.size(); i++) {
+ Ref ref = refs.get(i);
+ if (ref != null && ref.getObjectId() != null) {
+ if (info.branches == null) {
+ info.branches = new LinkedHashMap<>();
+ }
+ info.branches.put(showBranch.get(i), ref.getObjectId().name());
+ }
+ }
+ }
+
+ private List<Ref> retrieveBranchRefs(ProjectState e, Repository git) {
+ if (!e.statePermitsRead()) {
+ return ImmutableList.of();
+ }
+
+ return getBranchRefs(e.getNameKey(), git);
+ }
+
+ private void addParentProjectInfo(
+ Map<String, String> hiddenNames,
+ Map<Project.NameKey, Boolean> accessibleParents,
+ PermissionBackend.WithUser perm,
+ ProjectState e,
+ ProjectInfo info)
+ throws PermissionBackendException {
+ ProjectState parent = Iterables.getFirst(e.parents(), null);
+ if (parent != null) {
+ if (isParentAccessible(accessibleParents, perm, parent)) {
+ info.parent = parent.getName();
+ } else {
+ info.parent = hiddenNames.get(parent.getName());
+ if (info.parent == null) {
+ info.parent = "?-" + (hiddenNames.size() + 1);
+ hiddenNames.put(parent.getName(), info.parent);
+ }
+ }
+ }
+ }
+
+ private Stream<ProjectState> filter(PermissionBackend.WithUser perm) throws BadRequestException {
+ return StreamSupport.stream(scan().spliterator(), false)
+ .map(projectCache::get)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .filter(p -> permissionCheck(p, perm));
+ }
+
+ private boolean permissionCheck(ProjectState state, PermissionBackend.WithUser perm) {
+ // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+ // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+ // be allowed for other users). Allowing project owners to access here will help them to view
+ // and update the config of hidden projects easily.
+ return perm.project(state.getNameKey())
+ .testOrFalse(
+ state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG);
+ }
+
+ private boolean isParentAccessible(
+ Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState state)
+ throws PermissionBackendException {
+ Project.NameKey name = state.getNameKey();
+ Boolean b = checked.get(name);
+ if (b == null) {
+ try {
+ // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+ // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+ // be allowed for other users). Allowing project owners to access here will help them to
+ // view
+ // and update the config of hidden projects easily.
+ ProjectPermission permissionToCheck =
+ state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
+ perm.project(name).check(permissionToCheck);
+ b = true;
+ } catch (AuthException denied) {
+ b = false;
+ }
+ checked.put(name, b);
+ }
+ return b;
+ }
+
+ private Stream<Project.NameKey> scan() throws BadRequestException {
+ if (matchPrefix != null) {
+ checkMatchOptions(matchSubstring == null && matchRegex == null);
+ return projectCache.byName(matchPrefix).stream();
+ } else if (matchSubstring != null) {
+ checkMatchOptions(matchPrefix == null && matchRegex == null);
+ return projectCache.all().stream()
+ .filter(
+ p -> p.get().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US)));
+ } else if (matchRegex != null) {
+ checkMatchOptions(matchPrefix == null && matchSubstring == null);
+ RegexListSearcher<Project.NameKey> searcher;
+ try {
+ searcher = new RegexListSearcher<>(matchRegex, Project.NameKey::get);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException(e.getMessage());
+ }
+ return searcher.search(projectCache.all().asList());
+ } else {
+ return projectCache.all().stream();
+ }
+ }
+
+ private static void checkMatchOptions(boolean cond) throws BadRequestException {
+ if (!cond) {
+ throw new BadRequestException("specify exactly one of p/m/r");
+ }
+ }
+
+ private void printProjectTree(
+ final PrintWriter stdout, TreeMap<Project.NameKey, ProjectNode> treeMap) {
+ final NavigableSet<ProjectNode> sortedNodes = new TreeSet<>();
+
+ // Builds the inheritance tree using a list.
+ //
+ for (ProjectNode key : treeMap.values()) {
+ if (key.isAllProjects()) {
+ sortedNodes.add(key);
+ continue;
+ }
+
+ ProjectNode node = treeMap.get(key.getParentName());
+ if (node != null) {
+ node.addChild(key);
+ } else {
+ sortedNodes.add(key);
+ }
+ }
+
+ final TreeFormatter treeFormatter = new TreeFormatter(stdout);
+ treeFormatter.printTree(sortedNodes);
+ stdout.flush();
+ }
+
+ private List<Ref> getBranchRefs(Project.NameKey projectName, Repository git) {
+ Ref[] result = new Ref[showBranch.size()];
+ try {
+ PermissionBackend.ForProject perm = permissionBackend.user(currentUser).project(projectName);
+ for (int i = 0; i < showBranch.size(); i++) {
+ Ref ref = git.findRef(showBranch.get(i));
+ if (ref != null && ref.getObjectId() != null) {
+ try {
+ perm.ref(ref.getLeaf().getName()).check(RefPermission.READ);
+ result[i] = ref;
+ } catch (AuthException e) {
+ continue;
+ }
+ }
+ }
+ } catch (IOException | PermissionBackendException e) {
+ // Fall through and return what is available.
+ }
+ return Arrays.asList(result);
+ }
+
+ private static boolean hasValidRef(List<Ref> refs) {
+ for (Ref ref : refs) {
+ if (ref != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectNode.java b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
index 816c69d..ff3f588 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectNode.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
@@ -22,7 +22,7 @@
import java.util.NavigableSet;
import java.util.TreeSet;
-/** Node of a Project in a tree formatted by {@link ListProjects}. */
+/** Node of a Project in a tree formatted by {@link ListProjectsImpl}. */
public class ProjectNode implements TreeNode, Comparable<ProjectNode> {
public interface Factory {
ProjectNode create(Project project, boolean isVisible);
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
index d188bc8..3883c8c6 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
@@ -25,60 +25,89 @@
import static com.google.gerrit.server.project.TagResource.TAG_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.server.config.GerritConfigListener;
-import com.google.gerrit.server.project.RefValidationHelper;
import com.google.gerrit.server.restapi.change.CherryPickCommit;
-import com.google.gerrit.server.validators.ProjectCreationValidationListener;
public class ProjectRestApiModule extends RestApiModule {
@Override
protected void configure() {
bind(ProjectsCollection.class);
+ bind(ListProjects.class).to(ListProjectsImpl.class);
bind(DashboardsCollection.class);
- DynamicMap.mapOf(binder(), PROJECT_KIND);
- DynamicMap.mapOf(binder(), CHILD_PROJECT_KIND);
DynamicMap.mapOf(binder(), BRANCH_KIND);
+ DynamicMap.mapOf(binder(), CHILD_PROJECT_KIND);
+ DynamicMap.mapOf(binder(), COMMIT_KIND);
DynamicMap.mapOf(binder(), DASHBOARD_KIND);
DynamicMap.mapOf(binder(), FILE_KIND);
- DynamicMap.mapOf(binder(), COMMIT_KIND);
- DynamicMap.mapOf(binder(), TAG_KIND);
DynamicMap.mapOf(binder(), LABEL_KIND);
+ DynamicMap.mapOf(binder(), PROJECT_KIND);
DynamicMap.mapOf(binder(), SUBMIT_REQUIREMENT_KIND);
-
- DynamicSet.bind(binder(), GerritConfigListener.class).to(SetParent.class);
- DynamicSet.bind(binder(), ProjectCreationValidationListener.class)
- .to(CreateProject.ValidBranchListener.class);
+ DynamicMap.mapOf(binder(), TAG_KIND);
create(PROJECT_KIND).to(CreateProject.class);
- put(PROJECT_KIND).to(PutProject.class);
get(PROJECT_KIND).to(GetProject.class);
- get(PROJECT_KIND, "description").to(GetDescription.class);
- put(PROJECT_KIND, "description").to(PutDescription.class);
- delete(PROJECT_KIND, "description").to(PutDescription.class);
-
+ put(PROJECT_KIND).to(PutProject.class);
get(PROJECT_KIND, "access").to(GetAccess.class);
post(PROJECT_KIND, "access").to(SetAccess.class);
put(PROJECT_KIND, "access:review").to(CreateAccessChange.class);
- get(PROJECT_KIND, "check.access").to(CheckAccess.class);
+ put(PROJECT_KIND, "ban").to(BanCommit.class);
+ child(PROJECT_KIND, "branches").to(BranchesCollection.class);
+ create(BRANCH_KIND).to(CreateBranch.class);
+ put(BRANCH_KIND).to(PutBranch.class);
+ get(BRANCH_KIND).to(GetBranch.class);
+ delete(BRANCH_KIND).to(DeleteBranch.class);
+
+ child(BRANCH_KIND, "files").to(FilesCollection.class);
+ get(FILE_KIND, "content").to(GetContent.class);
+
+ get(BRANCH_KIND, "mergeable").to(CheckMergeability.class);
+ get(BRANCH_KIND, "reflog").to(GetReflog.class);
+
+ post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
post(PROJECT_KIND, "check").to(Check.class);
-
- get(PROJECT_KIND, "parent").to(GetParent.class);
- put(PROJECT_KIND, "parent").to(SetParent.class);
+ get(PROJECT_KIND, "check.access").to(CheckAccess.class);
child(PROJECT_KIND, "children").to(ChildProjectsCollection.class);
get(CHILD_PROJECT_KIND).to(GetChildProject.class);
+ child(PROJECT_KIND, "commits").to(CommitsCollection.class);
+ get(COMMIT_KIND).to(GetCommit.class);
+ post(COMMIT_KIND, "cherrypick").to(CherryPickCommit.class);
+ child(COMMIT_KIND, "files").to(FilesInCommitCollection.class);
+ get(COMMIT_KIND, "in").to(CommitIncludedIn.class);
+
+ get(PROJECT_KIND, "commits:in").to(CommitsIncludedInRefs.class);
+
+ get(PROJECT_KIND, "config").to(GetConfig.class);
+ put(PROJECT_KIND, "config").to(PutConfig.class);
+
+ post(PROJECT_KIND, "create.change").to(CreateChange.class);
+
+ child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
+ create(DASHBOARD_KIND).to(CreateDashboard.class);
+ delete(DASHBOARD_KIND).to(DeleteDashboard.class);
+ get(DASHBOARD_KIND).to(GetDashboard.class);
+ put(DASHBOARD_KIND).to(SetDashboard.class);
+
+ get(PROJECT_KIND, "description").to(GetDescription.class);
+ put(PROJECT_KIND, "description").to(PutDescription.class);
+ delete(PROJECT_KIND, "description").to(PutDescription.class);
+ get(PROJECT_KIND, "HEAD").to(GetHead.class);
+ put(PROJECT_KIND, "HEAD").to(SetHead.class);
+ post(PROJECT_KIND, "index").to(Index.class);
+
child(PROJECT_KIND, "labels").to(LabelsCollection.class);
create(LABEL_KIND).to(CreateLabel.class);
+ postOnCollection(LABEL_KIND).to(PostLabels.class);
get(LABEL_KIND).to(GetLabel.class);
put(LABEL_KIND).to(SetLabel.class);
delete(LABEL_KIND).to(DeleteLabel.class);
- postOnCollection(LABEL_KIND).to(PostLabels.class);
+
+ get(PROJECT_KIND, "parent").to(GetParent.class);
+ put(PROJECT_KIND, "parent").to(SetParent.class);
child(PROJECT_KIND, "submit_requirements").to(SubmitRequirementsCollection.class);
create(SUBMIT_REQUIREMENT_KIND).to(CreateSubmitRequirement.class);
@@ -86,59 +115,22 @@
get(SUBMIT_REQUIREMENT_KIND).to(GetSubmitRequirement.class);
delete(SUBMIT_REQUIREMENT_KIND).to(DeleteSubmitRequirement.class);
- get(PROJECT_KIND, "HEAD").to(GetHead.class);
- put(PROJECT_KIND, "HEAD").to(SetHead.class);
-
- put(PROJECT_KIND, "ban").to(BanCommit.class);
-
- post(PROJECT_KIND, "index").to(Index.class);
-
- child(PROJECT_KIND, "branches").to(BranchesCollection.class);
- create(BRANCH_KIND).to(CreateBranch.class);
- post(PROJECT_KIND, "create.change").to(CreateChange.class);
- put(BRANCH_KIND).to(PutBranch.class);
- get(BRANCH_KIND).to(GetBranch.class);
- delete(BRANCH_KIND).to(DeleteBranch.class);
- post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
- get(BRANCH_KIND, "mergeable").to(CheckMergeability.class);
- factory(RefValidationHelper.Factory.class);
- get(BRANCH_KIND, "reflog").to(GetReflog.class);
- child(BRANCH_KIND, "files").to(FilesCollection.class);
- get(FILE_KIND, "content").to(GetContent.class);
-
- child(PROJECT_KIND, "commits").to(CommitsCollection.class);
- get(PROJECT_KIND, "commits:in").to(CommitsIncludedInRefs.class);
- get(COMMIT_KIND).to(GetCommit.class);
- get(COMMIT_KIND, "in").to(CommitIncludedIn.class);
- child(COMMIT_KIND, "files").to(FilesInCommitCollection.class);
-
child(PROJECT_KIND, "tags").to(TagsCollection.class);
create(TAG_KIND).to(CreateTag.class);
get(TAG_KIND).to(GetTag.class);
put(TAG_KIND).to(PutTag.class);
delete(TAG_KIND).to(DeleteTag.class);
+
post(PROJECT_KIND, "tags:delete").to(DeleteTags.class);
-
- child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
- create(DASHBOARD_KIND).to(CreateDashboard.class);
- get(DASHBOARD_KIND).to(GetDashboard.class);
- put(DASHBOARD_KIND).to(SetDashboard.class);
- delete(DASHBOARD_KIND).to(DeleteDashboard.class);
-
- get(PROJECT_KIND, "config").to(GetConfig.class);
- put(PROJECT_KIND, "config").to(PutConfig.class);
- post(COMMIT_KIND, "cherrypick").to(CherryPickCommit.class);
-
- factory(ProjectNode.Factory.class);
}
/** Separately bind batch functionality. */
public static class BatchModule extends RestApiModule {
@Override
protected void configure() {
- get(PROJECT_KIND, "statistics.git").to(GetStatistics.class);
post(PROJECT_KIND, "gc").to(GarbageCollect.class);
post(PROJECT_KIND, "index.changes").to(IndexChanges.class);
+ get(PROJECT_KIND, "statistics.git").to(GetStatistics.class);
}
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index f9602bc..8917719 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -84,7 +84,10 @@
if (hasQuery) {
return queryProjects.get();
}
- return list.get().setFormat(OutputFormat.JSON);
+
+ ListProjects listProjects = list.get();
+ listProjects.setFormat(OutputFormat.JSON);
+ return listProjects;
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/project/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
index b219085..dc7499d 100644
--- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -16,6 +16,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -25,6 +26,7 @@
import com.google.gerrit.index.project.ProjectData;
import com.google.gerrit.index.project.ProjectIndex;
import com.google.gerrit.index.project.ProjectIndexCollection;
+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.project.ProjectJson;
@@ -47,6 +49,7 @@
private int limit;
private int start;
+ @CanIgnoreReturnValue
@Option(
name = "--query",
aliases = {"-q"},
@@ -56,6 +59,7 @@
return this;
}
+ @CanIgnoreReturnValue
@Option(
name = "--limit",
aliases = {"-n"},
@@ -66,6 +70,7 @@
return this;
}
+ @CanIgnoreReturnValue
@Option(
name = "--start",
aliases = {"-S"},
@@ -95,10 +100,6 @@
}
public List<ProjectInfo> apply() throws BadRequestException, MethodNotAllowedException {
- if (Strings.isNullOrEmpty(query)) {
- throw new BadRequestException("missing query field");
- }
-
ProjectIndex searchIndex = indexes.getSearchIndex();
if (searchIndex == null) {
throw new MethodNotAllowedException("no project index");
@@ -119,16 +120,21 @@
}
try {
- QueryResult<ProjectData> result = queryProcessor.query(queryBuilder.parse(query));
+ QueryResult<ProjectData> result =
+ queryProcessor.query(
+ !Strings.isNullOrEmpty(query) ? queryBuilder.parse(query) : Predicate.any());
List<ProjectData> pds = result.entities();
ArrayList<ProjectInfo> projectInfos = Lists.newArrayListWithCapacity(pds.size());
for (ProjectData pd : pds) {
projectInfos.add(json.format(pd.getProject()));
}
+ if (!projectInfos.isEmpty() && result.more()) {
+ projectInfos.get(projectInfos.size() - 1)._moreProjects = true;
+ }
return projectInfos;
} catch (QueryParseException e) {
- throw new BadRequestException(e.getMessage());
+ throw new BadRequestException(e.getMessage(), e);
}
}
}
diff --git a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index e711d57..7660eeb 100644
--- a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -16,7 +16,7 @@
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
-import com.google.gerrit.server.restapi.project.ListProjects;
+import com.google.gerrit.server.restapi.project.ListProjectsImpl;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.util.cli.Options;
@@ -28,7 +28,7 @@
description = "List projects visible to the caller",
runsAt = MASTER_OR_SLAVE)
public class ListProjectsCommand extends SshCommand {
- @Inject @Options public ListProjects impl;
+ @Inject @Options public ListProjectsImpl impl;
@Override
public void run() throws Exception {
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 5b89228..5d1a402 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -74,9 +74,6 @@
public void stop() {}
}
- @Option(name = "--gc", usage = "perform Java GC before printing memory stats")
- private boolean gc;
-
@Option(name = "--show-jvm", usage = "show details about the JVM")
private boolean showJVM;
@@ -141,8 +138,7 @@
if (showJvm) {
sshSummary();
- SummaryInfo summary =
- getSummary.setGc(gc).setJvm(showJVM).apply(new ConfigResource()).value();
+ SummaryInfo summary = getSummary.setJvm(showJVM).apply(new ConfigResource()).value();
taskSummary(summary.taskSummary);
memSummary(summary.memSummary);
threadSummary(summary.threadSummary);
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 81a6443..7466c67 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -8,10 +8,12 @@
exclude = [
"AssertableExecutorService.java",
"TestActionRefUpdateContext.java",
+ "GerritJUnit.java",
],
),
visibility = ["//visibility:public"],
exports = [
+ ":gerrit-junit",
"//lib:junit",
"//lib/mockito",
],
@@ -61,6 +63,12 @@
)
java_library(
+ name = "gerrit-junit",
+ srcs = ["GerritJUnit.java"],
+ visibility = ["//visibility:public"],
+)
+
+java_library(
# This can't be part of gerrit-test-util because of https://github.com/google/guava/issues/2837
name = "assertable-executor",
testonly = True,
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 6d046a3..e4e390e 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -85,7 +85,6 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.PerThreadRequestScope;
import com.google.gerrit.server.git.SearchingChangeCacheImpl.SearchingChangeCacheImplModule;
-import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.WorkQueue.WorkQueueModule;
import com.google.gerrit.server.group.testing.TestGroupBackend;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
@@ -144,7 +143,6 @@
cfg.setString(
"accountPatchReviewDb", null, "url", JdbcAccountPatchReviewStore.TEST_IN_MEMORY_URL);
cfg.setEnum("auth", null, "type", AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT);
- cfg.setString("gerrit", null, "allProjects", "Test-Projects");
cfg.setString("gerrit", null, "basePath", "git");
cfg.setString("gerrit", null, "canonicalWebUrl", "http://test/");
cfg.setString("user", null, "name", "Gerrit Code Review");
@@ -152,6 +150,7 @@
cfg.unset("cache", null, "directory");
cfg.setBoolean("index", "lucene", "testInmemory", true);
cfg.setInt("sendemail", null, "threadPoolSize", 0);
+ cfg.setInt("execution", null, "fanOutThreadPoolSize", 0);
cfg.setBoolean("receive", null, "enableSignedPush", false);
cfg.setString("receive", null, "certNonceSeed", "sekret");
}
@@ -344,8 +343,8 @@
@Provides
@Singleton
@FanOutExecutor
- public ExecutorService createFanOutExecutor(WorkQueue queues) {
- return queues.createQueue(2, "FanOut");
+ public ExecutorService createFanOutExecutor() {
+ return newDirectExecutorService();
}
@Provides
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 6360642..e814138 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -32,7 +32,6 @@
import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithExpiration;
import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithSecondUserId;
import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration;
-import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
import static com.google.gerrit.server.account.AccountProperties.ACCOUNT;
import static com.google.gerrit.server.account.AccountProperties.ACCOUNT_CONFIG;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
@@ -142,9 +141,9 @@
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdsNoteDbImpl;
import com.google.gerrit.server.account.storage.notedb.AccountsUpdateNoteDbImpl;
import com.google.gerrit.server.change.AccountPatchReviewStore;
@@ -250,7 +249,7 @@
@Inject private ExtensionRegistry extensionRegistry;
@Inject private PluginSetContext<ExceptionHook> exceptionHooks;
@Inject private ExternalIdKeyFactory externalIdKeyFactory;
- @Inject private ExternalIdFactory externalIdFactory;
+ @Inject private ExternalIdFactoryNoteDbImpl externalIdFactory;
@Inject private AuthConfig authConfig;
@Inject private AccountControl.Factory accountControlFactory;
@@ -798,7 +797,6 @@
gApi.accounts().self().starChange(triplet);
ChangeInfo change = info(triplet);
assertThat(change.starred).isTrue();
- assertThat(change.stars).contains(DEFAULT_LABEL);
refUpdateCounter.assertRefUpdateFor(
RefUpdateCounter.projectRef(
allUsers, RefNames.refsStarredChanges(Change.id(change._number), admin.id())));
@@ -806,7 +804,6 @@
gApi.accounts().self().unstarChange(triplet);
change = info(triplet);
assertThat(change.starred).isNull();
- assertThat(change.stars).isNull();
refUpdateCounter.assertRefUpdateFor(
RefUpdateCounter.projectRef(
allUsers, RefNames.refsStarredChanges(Change.id(change._number), admin.id())));
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index 0d246e3..091d444 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -41,8 +41,8 @@
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.notedb.Sequences;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index d9b01ad..b1cc866 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -31,9 +31,13 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.accounts.AccountInput;
@@ -64,7 +68,7 @@
private static final String DESTINATION_BRANCH = "destBranch";
private static final String ADDED_FILE_NAME = "a_new_file.txt";
- private static final String ADDED_FILE_CONTENT = "First added line\nSecond added line\n";
+ private static final String ADDED_FILE_CONTENT = "First added line\nSecond added line";
private static final String ADDED_FILE_DIFF =
"diff --git a/a_new_file.txt b/a_new_file.txt\n"
+ "new file mode 100644\n"
@@ -72,10 +76,13 @@
+ "+++ b/a_new_file.txt\n"
+ "@@ -0,0 +1,2 @@\n"
+ "+First added line\n"
- + "+Second added line\n";
+ + "+Second added line\n"
+ + "\\ No newline at end of file\n";
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ChangeOperations changeOperations;
+ @Inject private AccountOperations accountOperations;
@Test
public void applyAddedFilePatch_success() throws Exception {
@@ -88,6 +95,27 @@
assertDiffForNewFile(diff, result.currentRevision, ADDED_FILE_NAME, ADDED_FILE_CONTENT);
}
+ @Test
+ public void applyAddedFilePatchAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change
+ Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Apply patch
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.responseFormatOptions = ImmutableList.of(CURRENT_REVISION, CURRENT_COMMIT);
+ ChangeInfo result = gApi.changes().id(change.get()).applyPatch(in);
+
+ assertThat(result.getCurrentRevision().commit.committer.email).isEqualTo(emailTwo);
+ }
+
private static final String MODIFIED_FILE_NAME = "modified_file.txt";
private static final String MODIFIED_FILE_ORIGINAL_CONTENT =
"First original line\nSecond original line";
@@ -102,6 +130,13 @@
+ "+Modified line\n";
@Test
+ public void applyPatchWithoutProvidingPatch_badRequest() throws Exception {
+ initBaseWithFile(MODIFIED_FILE_NAME, MODIFIED_FILE_ORIGINAL_CONTENT);
+ Throwable error = assertThrows(BadRequestException.class, () -> applyPatch(buildInput(null)));
+ assertThat(error).hasMessageThat().isEqualTo("patch required");
+ }
+
+ @Test
public void applyModifiedFilePatch_success() throws Exception {
initBaseWithFile(MODIFIED_FILE_NAME, MODIFIED_FILE_ORIGINAL_CONTENT);
ApplyPatchPatchSetInput in = buildInput(MODIFIED_FILE_DIFF);
@@ -498,6 +533,38 @@
}
@Test
+ public void commitMessage_providedMessageWithCorrectChangeId() throws Exception {
+ initDestBranch();
+ String originalChangeId =
+ gApi.changes()
+ .create(new ChangeInput(project.get(), DESTINATION_BRANCH, "Default commit message"))
+ .info()
+ .changeId;
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.commitMessage = "custom commit message\n\nChange-Id: " + originalChangeId + "\n";
+
+ ChangeInfo result = gApi.changes().id(originalChangeId).applyPatch(in);
+
+ ChangeInfo info = get(result.changeId, CURRENT_REVISION, CURRENT_COMMIT);
+ assertThat(info.revisions.get(info.currentRevision).commit.message).isEqualTo(in.commitMessage);
+ }
+
+ @Test
+ public void commitMessage_providedMessageWithWrongChangeId() throws Exception {
+ initDestBranch();
+ String originalChangeId =
+ gApi.changes()
+ .create(new ChangeInput(project.get(), DESTINATION_BRANCH, "Default commit message"))
+ .info()
+ .changeId;
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.commitMessage = "custom commit message\n\nChange-Id: " + "I1234567890" + "\n";
+
+ assertThrows(
+ ResourceConflictException.class, () -> gApi.changes().id(originalChangeId).applyPatch(in));
+ }
+
+ @Test
public void commitMessage_defaultMessageAndPatchHeader() throws Exception {
initDestBranch();
ApplyPatchPatchSetInput in = buildInput("Patch header\n" + ADDED_FILE_DIFF);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index ec474f1..97ec978 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -48,7 +48,6 @@
import static com.google.gerrit.extensions.client.ReviewerState.CC;
import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
-import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
@@ -89,6 +88,7 @@
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.server.change.CommentsUtil;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -160,7 +160,6 @@
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.git.ObjectIds;
-import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.server.ChangeMessagesUtil;
@@ -237,6 +236,14 @@
@Inject private ExtensionRegistry extensionRegistry;
@Inject private IndexOperations.Change changeIndexOperations;
@Inject private AccountControl.Factory accountControlFactory;
+ @Inject private ChangeOperations changeOperations;
+
+ public static final ImmutableSet<ListChangesOption> DASHBOARD_OPTIONS =
+ ImmutableSet.of(
+ ListChangesOption.LABELS,
+ ListChangesOption.DETAILED_ACCOUNTS,
+ ListChangesOption.SUBMIT_REQUIREMENTS,
+ ListChangesOption.STAR);
@Inject
@Named("diff_intraline")
@@ -507,6 +514,7 @@
.reviewer("byemail3@example.com", CC, false)
.reviewer("byemail4@example.com", CC, false);
ReviewResult result = gApi.changes().id(changeId).current().review(in);
+ assertThat(result.changeInfo).isNull();
assertThat(result.reviewers).isNotEmpty();
ChangeInfo info = gApi.changes().id(changeId).get();
Function<Collection<AccountInfo>, Collection<String>> toEmails =
@@ -2644,11 +2652,27 @@
@Test
public void queryChangesLimit() throws Exception {
- createChange();
- PushOneCommit.Result r2 = createChange();
- List<ChangeInfo> results = gApi.changes().query().withLimit(1).get();
- assertThat(results).hasSize(1);
- assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r2.getChangeId());
+ for (int i = 0; i < 3; i++) {
+ createChange();
+ }
+ List<ChangeInfo> resultsLimited = gApi.changes().query().withLimit(1).get();
+ List<ChangeInfo> resultsUnlimited = gApi.changes().query().get();
+ assertThat(resultsLimited).hasSize(1);
+ assertThat(resultsUnlimited.size()).isAtLeast(3);
+ }
+
+ @Test
+ @GerritConfig(name = "index.defaultLimit", value = "2")
+ public void queryChangesLimitDefault() throws Exception {
+ for (int i = 0; i < 4; i++) {
+ createChange();
+ }
+ List<ChangeInfo> resultsLimited = gApi.changes().query().withLimit(1).get();
+ List<ChangeInfo> resultsUnlimited = gApi.changes().query().get();
+ List<ChangeInfo> resultsLimitedAboveDefault = gApi.changes().query().withLimit(3).get();
+ assertThat(resultsLimited).hasSize(1);
+ assertThat(resultsUnlimited).hasSize(2);
+ assertThat(resultsLimitedAboveDefault).hasSize(3);
}
@Test
@@ -2847,6 +2871,23 @@
}
@Test
+ @GerritConfig(name = "change.topicLimit", value = "3")
+ public void topicSizeLimit() throws Exception {
+ for (int i = 0; i < 3; i++) {
+ createChangeWithTopic(testRepo, "limitedTopic", "message", "a.txt", "content\n");
+ }
+ PushOneCommit.Result rLimited =
+ pushFactory
+ .create(user.newIdent(), testRepo)
+ .to("refs/for/master%topic=" + name("limitedTopic"));
+ rLimited.assertErrorStatus("topicLimit");
+
+ PushOneCommit.Result rOther =
+ createChangeWithTopic(testRepo, "otherTopic", "message", "a.txt", "content\n");
+ assertThat(gApi.changes().id(rOther.getChangeId()).topic()).contains("otherTopic");
+ }
+
+ @Test
public void editTopicWithoutPermissionNotAllowed() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
@@ -3097,7 +3138,7 @@
gApi.changes()
.query()
.withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
- .withOptions(IndexPreloadingUtil.DASHBOARD_OPTIONS)
+ .withOptions(DASHBOARD_OPTIONS)
.get())
.hasSize(2);
}
@@ -3861,6 +3902,28 @@
}
@Test
+ public void changeCommitMessageAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change
+ Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Change commit message
+ ChangeInfo changeInfo = gApi.changes().id(change.get()).get();
+ String msg = String.format("New commit message\n\nChange-Id: %s\n", changeInfo.changeId);
+ gApi.changes().id(change.get()).setMessage(msg);
+
+ assertThat(gApi.changes().id(change.get()).get().getCurrentRevision().commit.committer.email)
+ .isEqualTo(emailTwo);
+ }
+
+ @Test
public void changeCommitMessageFromChangeIdToLinkFooter() throws Exception {
PushOneCommit.Result r = createChange();
r.assertOkStatus();
@@ -4301,14 +4364,12 @@
gApi.accounts().self().starChange(triplet);
ChangeInfo change = info(triplet);
assertThat(change.starred).isTrue();
- assertThat(change.stars).contains(DEFAULT_LABEL);
// change was not re-indexed
changeIndexedCounter.assertReindexOf(change, 0);
gApi.accounts().self().unstarChange(triplet);
change = info(triplet);
assertThat(change.starred).isNull();
- assertThat(change.stars).isNull();
// change was not re-indexed
changeIndexedCounter.assertReindexOf(change, 0);
}
@@ -4528,6 +4589,18 @@
testEmailSubjectContainsChangeSizeBucket(1000, "XL");
}
+ @Test
+ public void requestFormattedChangeInReview() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(r.getChange().change().isWorkInProgress()).isFalse();
+
+ ReviewInput in = ReviewInput.approve().reviewer(user.email()).label(LabelId.CODE_REVIEW, 1);
+ in.responseFormatOptions = ImmutableList.of(ListChangesOption.CURRENT_REVISION);
+ ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
+ assertThat(result.changeInfo).isNotNull();
+ assertThat(result.changeInfo.currentRevision).isNotNull();
+ }
+
private void testEmailSubjectContainsChangeSizeBucket(
int numberOfLines, String expectedSizeBucket) throws Exception {
String change;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java b/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java
index 2bde1652..771935a 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/CreateMergePatchSetIT.java
@@ -25,14 +25,21 @@
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.api.accounts.AccountInput;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -46,6 +53,10 @@
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.util.List;
@@ -58,6 +69,8 @@
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
+ @Inject private ChangeOperations changeOperations;
+ @Inject private AccountOperations accountOperations;
@Before
public void setUp() {
@@ -150,6 +163,35 @@
}
@Test
+ public void createMergePatchSetAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+ String branch = "dev";
+ createBranch(BranchNameKey.create(project, branch));
+
+ // Create a change for master branch
+ Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+
+ // Push a commit to dev branch
+ createChange("refs/heads/dev");
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Create merge patch-set
+ MergeInput mergeInput = new MergeInput();
+ mergeInput.source = branch;
+ MergePatchSetInput in = new MergePatchSetInput();
+ in.merge = mergeInput;
+ gApi.changes().id(change.get()).createMergePatchSet(in);
+
+ assertThat(gApi.changes().id(change.get()).get().getCurrentRevision().commit.committer.email)
+ .isEqualTo(emailTwo);
+ }
+
+ @Test
public void createMergePatchSet_Conflict() throws Exception {
RevCommit initialHead = projectOperations.project(project).getHead("master");
createBranch(BranchNameKey.create(project, "dev"));
@@ -361,6 +403,43 @@
}
@Test
+ public void createMergePatchSetWithValidationOption() throws Exception {
+ RevCommit initialHead = projectOperations.project(project).getHead("master");
+ createBranch(BranchNameKey.create(project, "dev"));
+
+ // create a change for master
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ // advance master branch
+ testRepo.reset(initialHead);
+ PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
+ currentMaster.assertOkStatus();
+
+ // push a commit into dev branch
+ testRepo.reset(initialHead);
+ PushOneCommit.Result changeA =
+ pushFactory
+ .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
+ .to("refs/heads/dev");
+ changeA.assertOkStatus();
+ MergeInput mergeInput = new MergeInput();
+ mergeInput.source = "dev";
+ MergePatchSetInput in = new MergePatchSetInput();
+ in.merge = mergeInput;
+ in.subject = "update change by merge ps2 inherit parent of ps1";
+ in.validationOptions = ImmutableMap.of("key", "value");
+
+ TestCommitValidationListener testCommitValidationListener = new TestCommitValidationListener();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(testCommitValidationListener)) {
+ gApi.changes().id(changeId).createMergePatchSet(in);
+ assertThat(testCommitValidationListener.receiveEvent.pushOptions)
+ .containsExactly("key", "value");
+ }
+ }
+
+ @Test
public void createMergePatchSetCannotBaseOnInvisibleChange() throws Exception {
RevCommit initialHead = projectOperations.project(project).getHead("master");
createBranch(BranchNameKey.create(project, "foo"));
@@ -505,6 +584,7 @@
assertThat(commitInfo.message).contains(subject);
assertThat(commitInfo.author.name).isEqualTo("Other Author");
assertThat(commitInfo.author.email).isEqualTo("otherauthor@example.com");
+ assertThat(commitInfo.committer.email).isEqualTo(admin.email());
}
@Test
@@ -652,4 +732,15 @@
event.getChange().workInProgress != null ? event.getChange().workInProgress : false;
}
}
+
+ private static class TestCommitValidationListener implements CommitValidationListener {
+ public CommitReceivedEvent receiveEvent;
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ this.receiveEvent = receiveEvent;
+ return ImmutableList.of();
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
index 785186d..7fe79e4 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseChainOnBehalfOfUploaderIT.java
@@ -209,6 +209,76 @@
}
@Test
+ public void rebaseChainOnBehalfOfUploaderAfterUpdatingPreferredEmailForUploader()
+ throws Exception {
+ // Create a chain of changes for being rebased
+ String uploaderEmailOne = "uploader1@example.com";
+ Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmailOne).create();
+ Change.Id changeToBeRebased1 =
+ changeOperations.newChange().project(project).owner(uploader).create();
+
+ Change.Id changeToBeRebased2 =
+ changeOperations
+ .newChange()
+ .project(project)
+ .childOf()
+ .change(changeToBeRebased1)
+ .owner(uploader)
+ .create();
+
+ Change.Id changeToBeRebased3 =
+ changeOperations
+ .newChange()
+ .project(project)
+ .childOf()
+ .change(changeToBeRebased2)
+ .owner(uploader)
+ .create();
+
+ // Change preferred email for the uploader
+ String uploaderEmailTwo = "uploader2@example.com";
+ accountOperations.account(uploader).forUpdate().preferredEmail(uploaderEmailTwo).update();
+
+ // Create, approve and submit the change that will be the new base for the chain that will be
+ // rebased
+ Change.Id changeToBeTheNewBase = changeOperations.newChange().project(project).create();
+ gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
+
+ // Rebase the chain on behalf of the uploader through changeToBeRebased3
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.onBehalfOfUploader = true;
+ gApi.changes().id(changeToBeRebased3.get()).rebaseChain(rebaseInput);
+ assertThat(
+ gApi.changes()
+ .id(changeToBeRebased1.get())
+ .get()
+ .getCurrentRevision()
+ .commit
+ .committer
+ .email)
+ .isEqualTo(uploaderEmailTwo);
+ assertThat(
+ gApi.changes()
+ .id(changeToBeRebased2.get())
+ .get()
+ .getCurrentRevision()
+ .commit
+ .committer
+ .email)
+ .isEqualTo(uploaderEmailTwo);
+ assertThat(
+ gApi.changes()
+ .id(changeToBeRebased3.get())
+ .get()
+ .getCurrentRevision()
+ .commit
+ .committer
+ .email)
+ .isEqualTo(uploaderEmailTwo);
+ }
+
+ @Test
public void rebaseChainOnBehalfOfUploaderMultipleTimesInARow() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
index ade7dc6..152d9dd 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -36,10 +36,12 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestMetricMaker;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.PatchSet;
@@ -97,6 +99,7 @@
@Inject protected ProjectOperations projectOperations;
@Inject protected ExtensionRegistry extensionRegistry;
@Inject protected TestMetricMaker testMetricMaker;
+ @Inject protected AccountOperations accountOperations;
@FunctionalInterface
protected interface RebaseCall {
@@ -214,6 +217,30 @@
}
@Test
+ public void rebaseChangeAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create two changes both with the same parent
+ Change.Id c1 = changeOperations.newChange().project(project).owner(testUser).create();
+ Change.Id c2 = changeOperations.newChange().project(project).owner(testUser).create();
+
+ // Approve and submit the first change
+ gApi.changes().id(c1.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(c1.get()).current().submit();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Rebase the second change
+ gApi.changes().id(c2.get()).rebase();
+ assertThat(gApi.changes().id(c2.get()).get().getCurrentRevision().commit.committer.email)
+ .isEqualTo(emailTwo);
+ }
+
+ @Test
public void cannotRebaseOntoBaseThatIsNotPresentInTargetBranch() throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
index 0fb8a82..3456012 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseOnBehalfOfUploaderIT.java
@@ -251,6 +251,41 @@
}
@Test
+ public void rebaseChangeOnBehalfOfUploaderAfterUpdatingPreferredEmailForUploader()
+ throws Exception {
+ String uploaderEmailOne = "uploader1@example.com";
+ Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmailOne).create();
+
+ // Create two changes both with the same parent
+ Change.Id changeToBeTheNewBase =
+ changeOperations.newChange().project(project).owner(uploader).create();
+ Change.Id changeToBeRebased =
+ changeOperations.newChange().project(project).owner(uploader).create();
+
+ // Approve and submit the change that will be the new base for the change that will be rebased.
+ gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(changeToBeTheNewBase.get()).current().submit();
+
+ // Change preferred email for the uploader
+ String uploaderEmailTwo = "uploader2@example.com";
+ accountOperations.account(uploader).forUpdate().preferredEmail(uploaderEmailTwo).update();
+
+ // Rebase the second change on behalf of the uploader
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.onBehalfOfUploader = true;
+ gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput);
+ assertThat(
+ gApi.changes()
+ .id(changeToBeRebased.get())
+ .get()
+ .getCurrentRevision()
+ .commit
+ .committer
+ .email)
+ .isEqualTo(uploaderEmailTwo);
+ }
+
+ @Test
public void rebaseChangeOnBehalfOfUploaderMultipleTimesInARow() throws Exception {
allowPermissionToAllUsers(Permission.REBASE);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
index ab2f358..d1e6bcba 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
@@ -70,7 +70,6 @@
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.TestLabels;
@@ -103,6 +102,13 @@
@Inject private ExtensionRegistry extensionRegistry;
@Inject private IndexOperations.Change changeIndexOperations;
+ private static final ImmutableSet<ListChangesOption> DASHBOARD_OPTIONS =
+ ImmutableSet.of(
+ ListChangesOption.LABELS,
+ ListChangesOption.DETAILED_ACCOUNTS,
+ ListChangesOption.SUBMIT_REQUIREMENTS,
+ ListChangesOption.STAR);
+
@Test
public void submitRecords() throws Exception {
PushOneCommit.Result r = createChange();
@@ -2952,7 +2958,7 @@
.withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
.withOptions(
new ImmutableSet.Builder<ListChangesOption>()
- .addAll(IndexPreloadingUtil.DASHBOARD_OPTIONS)
+ .addAll(DASHBOARD_OPTIONS)
.add(ListChangesOption.SUBMIT_REQUIREMENTS)
.build())
.get();
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 9456a31..6dbbe9a 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -1285,16 +1285,24 @@
}
@Test
- public void pushToGroupBranchForReviewForAllUsersRepoIsRejectedOnSubmit() throws Throwable {
+ public void pushToGroupBranchForReviewForAllUsersRepoIsRejectedOnSubmitForGroupFiles()
+ throws Throwable {
+ String error = "update to group files (group.config, members, subgroups) not allowed";
pushToGroupBranchForReviewAndSubmit(
- allUsers, RefNames.refsGroups(adminGroupUuid()), "group update not allowed");
+ allUsers, RefNames.refsGroups(adminGroupUuid()), "group.config", error);
+ pushToGroupBranchForReviewAndSubmit(
+ allUsers, RefNames.refsGroups(adminGroupUuid()), "members", error);
+ pushToGroupBranchForReviewAndSubmit(
+ allUsers, RefNames.refsGroups(adminGroupUuid()), "subgroups", error);
+ pushToGroupBranchForReviewAndSubmit(
+ allUsers, RefNames.refsGroups(adminGroupUuid()), "destinations/myreviews", null);
}
@Test
public void pushToGroupBranchForReviewForNonAllUsersRepoAndSubmit() throws Throwable {
String groupRef = RefNames.refsGroups(adminGroupUuid());
createBranch(project, groupRef);
- pushToGroupBranchForReviewAndSubmit(project, groupRef, null);
+ pushToGroupBranchForReviewAndSubmit(project, groupRef, "group.config", null);
}
@Test
@@ -1576,7 +1584,8 @@
}
private void pushToGroupBranchForReviewAndSubmit(
- Project.NameKey project, String groupRef, String expectedError) throws Throwable {
+ Project.NameKey project, String groupRef, String fileName, String expectedError)
+ throws Throwable {
projectOperations
.project(project)
.forUpdate()
@@ -1594,7 +1603,7 @@
PushOneCommit.Result r =
pushFactory
- .create(admin.newIdent(), repo, "Update group config", "group.config", "some content")
+ .create(admin.newIdent(), repo, "Update group config", fileName, "some content")
.to(MagicBranch.NEW_CHANGE + groupRef);
r.assertOkStatus();
assertThat(r.getChange().change().getDest().branch()).isEqualTo(groupRef);
@@ -1603,7 +1612,7 @@
ThrowingRunnable submit = () -> gApi.changes().id(r.getChangeId()).current().submit();
if (expectedError != null) {
Throwable thrown = assertThrows(ResourceConflictException.class, submit);
- assertThat(thrown).hasMessageThat().contains("group update not allowed");
+ assertThat(thrown).hasMessageThat().contains(expectedError);
} else {
submit.run();
}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 5c46fec..27193dd 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -59,6 +59,7 @@
private Project.NameKey normalProject;
private Project.NameKey secretProject;
private Project.NameKey secretRefProject;
+ private AccountGroup.UUID privilegedGroupUuid;
private TestAccount privilegedUser;
@Before
@@ -66,8 +67,7 @@
normalProject = projectOperations.newProject().create();
secretProject = projectOperations.newProject().create();
secretRefProject = projectOperations.newProject().create();
- AccountGroup.UUID privilegedGroupUuid =
- groupOperations.newGroup().name(name("privilegedGroup")).create();
+ privilegedGroupUuid = groupOperations.newGroup().name(name("privilegedGroup")).create();
privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden", null);
groupOperations.group(privilegedGroupUuid).forUpdate().addMember(privilegedUser.id()).update();
@@ -239,7 +239,9 @@
ImmutableList.of(
"'user1' can perform 'read' with force=false on project '"
+ normalProject.get()
- + "' for ref 'refs/heads/*'",
+ + "' for ref 'refs/heads/*' (allowed for group '"
+ + SystemGroupBackend.ANONYMOUS_USERS.get()
+ + "' by rule 'group Anonymous Users')",
"'user1' cannot perform 'viewPrivateChanges' with force=false on project '"
+ normalProject.get()
+ "' for ref 'refs/heads/master'")),
@@ -251,7 +253,9 @@
ImmutableList.of(
"'user1' can perform 'read' with force=false on project '"
+ normalProject.get()
- + "' for ref 'refs/heads/*'")),
+ + "' for ref 'refs/heads/*' (allowed for group '"
+ + SystemGroupBackend.ANONYMOUS_USERS.get()
+ + "' by rule 'group Anonymous Users')")),
// Test 3
TestCase.project(
user.email(),
@@ -273,7 +277,13 @@
ImmutableList.of(
"'user1' can perform 'read' with force=false on project '"
+ secretRefProject.get()
- + "' for ref 'refs/heads/*'",
+ + "' for ref 'refs/heads/*' (allowed for group '"
+ + SystemGroupBackend.REGISTERED_USERS.get()
+ // if the permission was assigned through ProjectOperations the local group
+ // name is set to the UUID
+ + "' by rule 'group "
+ + SystemGroupBackend.REGISTERED_USERS.get()
+ + "')",
"'user1' cannot perform 'read' with force=false on project '"
+ secretRefProject.get()
+ "' for ref 'refs/heads/secret/master' because this permission is blocked")),
@@ -286,10 +296,22 @@
ImmutableList.of(
"'privilegedUser' can perform 'read' with force=false on project '"
+ secretRefProject.get()
- + "' for ref 'refs/heads/*'",
+ + "' for ref 'refs/heads/*' (allowed for group '"
+ + SystemGroupBackend.REGISTERED_USERS.get()
+ // if the permission was assigned through ProjectOperations the local group
+ // name is set to the UUID
+ + "' by rule 'group "
+ + SystemGroupBackend.REGISTERED_USERS.get()
+ + "')",
"'privilegedUser' can perform 'read' with force=false on project '"
+ secretRefProject.get()
- + "' for ref 'refs/heads/secret/master'")),
+ + "' for ref 'refs/heads/secret/master' (allowed for group '"
+ + privilegedGroupUuid.get()
+ // if the permission was assigned through ProjectOperations the local group
+ // name is set to the UUID
+ + "' by rule 'group "
+ + privilegedGroupUuid.get()
+ + "')")),
// Test 6
TestCase.projectRef(
privilegedUser.email(),
@@ -299,7 +321,9 @@
ImmutableList.of(
"'privilegedUser' can perform 'read' with force=false on project '"
+ normalProject.get()
- + "' for ref 'refs/heads/*'")),
+ + "' for ref 'refs/heads/*' (allowed for group '"
+ + SystemGroupBackend.ANONYMOUS_USERS.get()
+ + "' by rule 'group Anonymous Users')")),
// Test 7
TestCase.projectRef(
privilegedUser.email(),
@@ -309,7 +333,13 @@
ImmutableList.of(
"'privilegedUser' can perform 'read' with force=false on project '"
+ secretProject.get()
- + "' for ref 'refs/*'")),
+ + "' for ref 'refs/*' (allowed for group '"
+ + privilegedGroupUuid.get()
+ // if the permission was assigned through ProjectOperations the local group
+ // name is set to the UUID
+ + "' by rule 'group "
+ + privilegedGroupUuid.get()
+ + "')")),
// Test 8
TestCase.projectRefPerm(
privilegedUser.email(),
@@ -320,10 +350,18 @@
ImmutableList.of(
"'privilegedUser' can perform 'read' with force=false on project '"
+ normalProject.get()
- + "' for ref 'refs/heads/*'",
+ + "' for ref 'refs/heads/*' (allowed for group '"
+ + SystemGroupBackend.ANONYMOUS_USERS.get()
+ + "' by rule 'group Anonymous Users')",
"'privilegedUser' can perform 'viewPrivateChanges' with force=false on project '"
+ normalProject.get()
- + "' for ref 'refs/heads/master'")),
+ + "' for ref 'refs/heads/master' (allowed for group '"
+ + privilegedGroupUuid.get()
+ // if the permission was assigned through ProjectOperations the local group
+ // name is set to the UUID
+ + "' by rule 'group "
+ + privilegedGroupUuid.get()
+ + "')")),
// Test 9
TestCase.projectRefPerm(
privilegedUser.email(),
@@ -334,10 +372,18 @@
ImmutableList.of(
"'privilegedUser' can perform 'read' with force=false on project '"
+ normalProject.get()
- + "' for ref 'refs/heads/*'",
+ + "' for ref 'refs/heads/*' (allowed for group '"
+ + SystemGroupBackend.ANONYMOUS_USERS.get()
+ + "' by rule 'group Anonymous Users')",
"'privilegedUser' can perform 'forgeServerAsCommitter' with force=false on project '"
+ normalProject.get()
- + "' for ref 'refs/heads/master'")));
+ + "' for ref 'refs/heads/master' (allowed for group '"
+ + privilegedGroupUuid.get()
+ // if the permission was assigned through ProjectOperations the local group
+ // name is set to the UUID
+ + "' by rule 'group "
+ + privilegedGroupUuid.get()
+ + "')")));
for (TestCase tc : inputs) {
String in = newGson().toJson(tc.input);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
index 5a024cc..e4dc0e83 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
@@ -29,8 +29,13 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
@@ -58,6 +63,9 @@
@NoHttpd
public class CommitIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
+ @Inject private AccountOperations accountOperations;
+ @Inject private ChangeOperations changeOperations;
+ @Inject private RequestScopeOperations requestScopeOperations;
@Test
public void getCommitInfo() throws Exception {
@@ -190,6 +198,40 @@
}
@Test
+ public void cherryPickCommitAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create target branch to cherry-pick to
+ String destBranch = "foo";
+ createBranch(BranchNameKey.create(project, destBranch));
+
+ // Create change to cherry-pick
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file("file")
+ .content("content")
+ .owner(testUser)
+ .create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Cherry-pick the change
+ String commit = gApi.changes().id(changeId.get()).get().getCurrentRevision().commit.commit;
+ CherryPickInput input = new CherryPickInput();
+ input.destination = destBranch;
+ input.message = "cherry-pick to foo branch";
+ ChangeInfo cherryPickResult =
+ gApi.projects().name(project.get()).commit(commit).cherryPick(input).get();
+ assertThat(cherryPickResult.getCurrentRevision().commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void cherryPickWithoutMessageOtherBranch() throws Exception {
String destBranch = "foo";
createBranch(BranchNameKey.create(project, destBranch));
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index f997c77..a93c0a5 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -1209,6 +1209,31 @@
.isNull();
}
+ @Test
+ public void queryProjectsLimit() throws Exception {
+ for (int i = 0; i < 3; i++) {
+ projectOperations.newProject().create();
+ }
+ List<ProjectInfo> resultsLimited = gApi.projects().query().withLimit(1).get();
+ List<ProjectInfo> resultsUnlimited = gApi.projects().query().get();
+ assertThat(resultsLimited).hasSize(1);
+ assertThat(resultsUnlimited.size()).isAtLeast(3);
+ }
+
+ @Test
+ @GerritConfig(name = "index.defaultLimit", value = "2")
+ public void queryProjectsLimitDefault() throws Exception {
+ for (int i = 0; i < 4; i++) {
+ projectOperations.newProject().create();
+ }
+ List<ProjectInfo> resultsLimited = gApi.projects().query().withLimit(1).get();
+ List<ProjectInfo> resultsUnlimited = gApi.projects().query().get();
+ List<ProjectInfo> resultsLimitedAboveDefault = gApi.projects().query().withLimit(3).get();
+ assertThat(resultsLimited).hasSize(1);
+ assertThat(resultsUnlimited).hasSize(2);
+ assertThat(resultsLimitedAboveDefault).hasSize(3);
+ }
+
private CommentLinkInfo commentLinkInfo(String name, String match, String link) {
CommentLinkInfo info = new CommentLinkInfo();
info.name = name;
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java b/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java
index 7c0b713..fa4f568 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/ApplyProvidedFixIT.java
@@ -25,7 +25,12 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.RefNames;
@@ -52,6 +57,9 @@
public class ApplyProvidedFixIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
+ @Inject private AccountOperations accountOperations;
+ @Inject private ChangeOperations changeOperations;
+ @Inject private RequestScopeOperations requestScopeOperations;
private static final String FILE_NAME = "file_to_fix.txt";
private static final String FILE_NAME2 = "another_file_to_fix.txt";
@@ -95,6 +103,35 @@
}
@Test
+ public void applyProvidedFixAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change
+ Change.Id change =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file(FILE_NAME)
+ .content(FILE_CONTENT)
+ .owner(testUser)
+ .create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Apply fix
+ ApplyProvidedFixInput applyProvidedFixInput =
+ createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);
+ gApi.changes().id(change.get()).current().applyFix(applyProvidedFixInput);
+
+ EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
+ assertThat(editInfo.commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void applyProvidedFixRestAPItestForASimpleFix() throws Exception {
ApplyProvidedFixInput applyProvidedFixInput =
createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index cc7712b..cdcd044 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -49,6 +49,8 @@
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.Account;
@@ -133,6 +135,8 @@
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
+ @Inject private AccountOperations accountOperations;
+ @Inject private ChangeOperations changeOperations;
@Test
public void reviewTriplet() throws Exception {
@@ -351,6 +355,40 @@
}
@Test
+ public void cherryPickAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create target branch to cherry-pick to
+ String branch = "foo";
+ gApi.projects().name(project.get()).branch(branch).create(new BranchInput());
+
+ // Create change to cherry-pick
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file("file")
+ .content("content")
+ .owner(testUser)
+ .create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Cherry-pick the change
+ String commit = gApi.changes().id(changeId.get()).get().getCurrentRevision().commit.commit;
+ CherryPickInput input = new CherryPickInput();
+ input.destination = branch;
+ input.message = "cherry-pick to foo branch";
+ ChangeInfo changeInfo =
+ gApi.changes().id(changeId.get()).revision(commit).cherryPick(input).get();
+ assertThat(changeInfo.getCurrentRevision().commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void cherryPickWithoutMessage() throws Exception {
String branch = "foo";
@@ -1718,6 +1756,34 @@
}
@Test
+ @GerritConfig(name = "change.maxFileSizeDownload", value = "10")
+ public void content_maxFileSizeDownload() throws Exception {
+ Map<String, String> files =
+ ImmutableMap.of("dir/file1.txt", " 9 bytes ", "dir/file2.txt", "11 bytes xx");
+ PushOneCommit.Result result =
+ pushFactory.create(admin.newIdent(), testRepo, SUBJECT, files).to("refs/for/master");
+ result.assertOkStatus();
+
+ // 9 bytes should be fine, because the limit is 10 bytes.
+ assertContent(result, "dir/file1.txt", " 9 bytes ");
+
+ // 11 bytes should throw, because the limit is 10 bytes.
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.changes()
+ .id(result.getChangeId())
+ .revision(result.getCommit().name())
+ .file("dir/file2.txt")
+ .content());
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ "File too big. File size: 11 bytes. Configured 'maxFileSizeDownload' limit: 10 bytes.");
+ }
+
+ @Test
public void patchsetLevelContentDoesNotExist() throws Exception {
PushOneCommit.Result change = createChange();
assertThrows(
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 1363ce7..1ab74fb 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -34,7 +34,10 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
@@ -73,6 +76,8 @@
public class RobotCommentsIT extends AbstractDaemonTest {
@Inject private TestCommentHelper testCommentHelper;
@Inject private ChangeOperations changeOperations;
+ @Inject private AccountOperations accountOperations;
+ @Inject private RequestScopeOperations requestScopeOperations;
private static final String PLAIN_TEXT_CONTENT_TYPE = "text/plain";
private static final String GERRIT_COMMIT_MESSAGE_TYPE = "text/x-gerrit-commit-message";
@@ -755,6 +760,46 @@
}
@Test
+ public void applyStoredFixAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change
+ Change.Id change =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file(FILE_NAME)
+ .content(FILE_CONTENT)
+ .owner(testUser)
+ .create();
+
+ // Add Robot Comment to the change
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+ testCommentHelper.addRobotComment(project + "~" + change.get(), withFixRobotCommentInput);
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Fetch Fix ID
+ List<RobotCommentInfo> robotCommentInfoList =
+ gApi.changes().id(change.get()).current().robotCommentsAsList();
+
+ List<String> fixIds = getFixIds(robotCommentInfoList);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ // Apply fix
+ gApi.changes().id(change.get()).current().applyFix(fixId);
+
+ EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
+ assertThat(editInfo.commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void storedFixSpanningMultipleLinesCanBeApplied() throws Exception {
fixReplacementInfo.path = FILE_NAME;
fixReplacementInfo.replacement = "Modified content\n5";
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 4168164..6bff0be 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -37,9 +37,13 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Patch;
@@ -112,6 +116,8 @@
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private AccountOperations accountOperations;
+ @Inject private ChangeOperations changeOperations;
private String changeId;
private String changeId2;
@@ -265,6 +271,30 @@
}
@Test
+ public void rebaseEditAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change to edit
+ Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Create edit
+ createEmptyEditFor(project + "~" + change.get());
+ // Add new patch-set to change
+ changeOperations.change(change).newPatchset().create();
+ // Rebase Edit
+ gApi.changes().id(change.get()).edit().rebase();
+
+ EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
+ assertThat(editInfo.commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void rebaseEditRest() throws Exception {
PatchSet previousPatchSet = getCurrentPatchSet(changeId2);
createEmptyEditFor(changeId2);
@@ -312,6 +342,33 @@
}
@Test
+ public void updateExistingFileAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change to edit
+ Change.Id change =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file(FILE_NAME)
+ .content("content")
+ .owner(testUser)
+ .create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Modify file
+ gApi.changes().id(change.get()).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
+
+ EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
+ assertThat(editInfo.commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void updateCommitMessageByEditingMagicCommitMsgFile() throws Exception {
createEmptyEditFor(changeId);
String updatedCommitMsg = "Foo Bar\n\nChange-Id: " + changeId + "\n";
@@ -455,6 +512,28 @@
}
@Test
+ public void updateMessageAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change to edit
+ Change.Id change = changeOperations.newChange().project(project).owner(testUser).create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Update commit message
+ ChangeInfo changeInfo = gApi.changes().id(change.get()).get();
+ String msg = String.format("New commit message\n\nChange-Id: %s\n", changeInfo.changeId);
+ gApi.changes().id(change.get()).edit().modifyCommitMessage(msg);
+
+ EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
+ assertThat(editInfo.commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void updateMessageRest() throws Exception {
adminRestSession.get(urlEditMessage(changeId, false)).assertNotFound();
EditMessage.Input in = new EditMessage.Input();
@@ -601,6 +680,33 @@
}
@Test
+ public void deleteExistingFileAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change to edit
+ Change.Id change =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file(FILE_NAME)
+ .content("content")
+ .owner(testUser)
+ .create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Delete file
+ gApi.changes().id(change.get()).edit().deleteFile(FILE_NAME);
+
+ EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
+ assertThat(editInfo.commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void renameExistingFile() throws Exception {
createEmptyEditFor(changeId);
gApi.changes().id(changeId).edit().renameFile(FILE_NAME, FILE_NAME3);
@@ -609,6 +715,33 @@
}
@Test
+ public void renameExistingFileAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change to edit
+ Change.Id change =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file(FILE_NAME)
+ .content("content")
+ .owner(testUser)
+ .create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Rename file
+ gApi.changes().id(change.get()).edit().renameFile(FILE_NAME, FILE_NAME3);
+
+ EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
+ assertThat(editInfo.commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void renameExistingFileToInvalidPath() throws Exception {
createEmptyEditFor(changeId);
BadRequestException badRequest =
@@ -644,6 +777,33 @@
}
@Test
+ public void restoreFileAfterUpdatingPreferredEmail() throws Exception {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Create change to edit
+ Change.Id change =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file(FILE_NAME)
+ .content("content")
+ .owner(testUser)
+ .create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Restore file to the state in the parent of change
+ gApi.changes().id(change.get()).edit().restoreFile(FILE_NAME);
+
+ EditInfo editInfo = gApi.changes().id(change.get()).edit().get().orElseThrow();
+ assertThat(editInfo.commit.committer.email).isEqualTo(emailTwo);
+ }
+
+ @Test
public void revertChanges() throws Exception {
createEmptyEditFor(changeId2);
gApi.changes().id(changeId2).edit().restoreFile(FILE_NAME);
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index e120f97..2ab054b 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -535,6 +535,16 @@
}
@Test
+ @GerritConfig(name = "change.topicLimit", value = "3")
+ public void pushForMasterWithTopicExceedsSizeLimitFails() throws Exception {
+ pushTo("refs/for/master%topic=limited").assertOkStatus();
+ pushTo("refs/for/master%topic=limited").assertOkStatus();
+ pushTo("refs/for/master%topic=limited").assertOkStatus();
+ PushOneCommit.Result r = pushTo("refs/for/master%topic=limited");
+ r.assertErrorStatus("topicLimit");
+ }
+
+ @Test
public void pushForMasterWithNotify() throws Exception {
// create a user that watches the project
TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", null);
@@ -1793,6 +1803,29 @@
}
@Test
+ @GerritConfig(name = "receive.enableChangeIdLinkFooters", value = "false")
+ public void pushWithLinkFooter_linkFootersDisabled() throws Exception {
+ String changeId = "I0123456789abcdef0123456789abcdef01234567";
+ String url = cfg.getString("gerrit", null, "canonicalWebUrl");
+ if (!url.endsWith("/")) {
+ url += "/";
+ }
+ createCommit(testRepo, "test commit\n\nLink: " + url + "id/" + changeId);
+ pushForReviewRejected(testRepo, "missing Change-Id in message footer");
+ }
+
+ @Test
+ @GerritConfig(name = "receive.enableChangeIdLinkFooters", value = "false")
+ public void pushWithChangeIdFooter_linkFootersDisabled() throws Exception {
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, null);
+
+ List<ChangeMessageInfo> messages = getMessages(r.getChangeId());
+ assertThat(messages.get(0).message).isEqualTo("Uploaded patch set 1.");
+ }
+
+ @Test
public void pushWithWrongHostLinkFooter() throws Exception {
String changeId = "I0123456789abcdef0123456789abcdef01234567";
createCommit(testRepo, "test commit\n\nLink: https://wronghost/id/" + changeId);
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ChangeExternalIdCaseSensitivityIT.java b/javatests/com/google/gerrit/acceptance/pgm/ChangeExternalIdCaseSensitivityIT.java
index 83df896..05cf12c 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ChangeExternalIdCaseSensitivityIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ChangeExternalIdCaseSensitivityIT.java
@@ -30,7 +30,7 @@
import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
diff --git a/javatests/com/google/gerrit/acceptance/rest/DynamicOptionsBeanParseListenerIT.java b/javatests/com/google/gerrit/acceptance/rest/DynamicOptionsBeanParseListenerIT.java
index 39f1e8d..f199b55 100644
--- a/javatests/com/google/gerrit/acceptance/rest/DynamicOptionsBeanParseListenerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/DynamicOptionsBeanParseListenerIT.java
@@ -21,7 +21,7 @@
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.DynamicOptions;
-import com.google.gerrit.server.restapi.project.ListProjects;
+import com.google.gerrit.server.restapi.project.ListProjectsImpl;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.inject.AbstractModule;
@@ -48,7 +48,7 @@
protected static class ListProjectsBeanListener implements DynamicOptions.BeanParseListener {
@Override
public void onBeanParseStart(String plugin, Object bean) {
- ListProjects listProjects = (ListProjects) bean;
+ ListProjectsImpl listProjects = (ListProjectsImpl) bean;
listProjects.setLimit(1);
}
@@ -60,7 +60,7 @@
@Override
public void configure() {
bind(DynamicOptions.DynamicBean.class)
- .annotatedWith(Exports.named(ListProjects.class))
+ .annotatedWith(Exports.named(ListProjectsImpl.class))
.to(ListProjectsBeanListener.class);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index f246a26..a76bf47 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -59,10 +59,10 @@
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdReader;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -105,7 +105,7 @@
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExternalIdKeyFactory externalIdKeyFactory;
- @Inject private ExternalIdFactory externalIdFactory;
+ @Inject private ExternalIdFactoryNoteDbImpl externalIdFactory;
@Inject private AllUsersName allUsersName;
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
index eed4d63..cfee7da 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
@@ -15,14 +15,17 @@
package com.google.gerrit.acceptance.rest.binding;
import static com.google.gerrit.acceptance.rest.util.RestApiCallHelper.execute;
+import static com.google.gerrit.acceptance.rest.util.RestCall.Method.GET;
import static com.google.gerrit.acceptance.rest.util.RestCall.Method.PUT;
import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration;
import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.UseSsh;
import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.config.GerritConfigs;
import com.google.gerrit.acceptance.rest.util.RestCall;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.extensions.common.ChangeInput;
@@ -53,51 +56,66 @@
ImmutableList.of(
RestCall.get("/accounts/%s"),
RestCall.put("/accounts/%s"),
- RestCall.get("/accounts/%s/detail"),
- RestCall.get("/accounts/%s/name"),
- RestCall.put("/accounts/%s/name"),
- RestCall.delete("/accounts/%s/name"),
- RestCall.get("/accounts/%s/username"),
- RestCall.builder(PUT, "/accounts/%s/username")
- // Changing the username is not allowed.
- .expectedResponseCode(SC_METHOD_NOT_ALLOWED)
- .expectedMessage("Username cannot be changed.")
- .build(),
RestCall.get("/accounts/%s/active"),
RestCall.put("/accounts/%s/active"),
RestCall.delete("/accounts/%s/active"),
- RestCall.put("/accounts/%s/password.http"),
- RestCall.delete("/accounts/%s/password.http"),
- RestCall.get("/accounts/%s/status"),
- RestCall.put("/accounts/%s/status"),
- RestCall.get("/accounts/%s/avatar"),
- RestCall.get("/accounts/%s/avatar.change.url"),
+ RestCall.get("/accounts/%s/agreements"),
+ RestCall.put("/accounts/%s/agreements"),
+
+ // TODO: The avatar REST endpoints always returns '404 Not Found' because no avatar plugin
+ // is installed.
+ RestCall.builder(GET, "/accounts/%s/avatar").expectedResponseCode(SC_NOT_FOUND).build(),
+ RestCall.builder(GET, "/accounts/%s/avatar.change.url")
+ .expectedResponseCode(SC_NOT_FOUND)
+ .build(),
+ RestCall.get("/accounts/%s/capabilities"),
+ RestCall.get("/accounts/%s/capabilities/viewPlugins"),
+ RestCall.put("/accounts/%s/displayname"),
+ RestCall.get("/accounts/%s/detail"),
+ RestCall.post("/accounts/%s/drafts:delete"),
RestCall.get("/accounts/%s/emails/"),
RestCall.put("/accounts/%s/emails/new-email@foo.com"),
- RestCall.get("/accounts/%s/sshkeys/"),
- RestCall.post("/accounts/%s/sshkeys/"),
- RestCall.get("/accounts/%s/watched.projects"),
- RestCall.post("/accounts/%s/watched.projects"),
- RestCall.post("/accounts/%s/watched.projects:delete"),
+ RestCall.get("/accounts/%s/external.ids"),
+ RestCall.post("/accounts/%s/external.ids:delete"),
+ RestCall.get("/accounts/%s/gpgkeys"),
+ RestCall.post("/accounts/%s/gpgkeys"),
RestCall.get("/accounts/%s/groups"),
+ RestCall.post("/accounts/%s/index"),
+ RestCall.get("/accounts/%s/name"),
+ RestCall.put("/accounts/%s/name"),
+ RestCall.delete("/accounts/%s/name"),
+
+ // TODO: The oauthtoken REST endpoint always returns '404 Not Found' because no oauth
+ // token is available for the test user.
+ RestCall.builder(GET, "/accounts/%s/oauthtoken")
+ .expectedResponseCode(SC_NOT_FOUND)
+ .build(),
+
+ // The password.http REST endpoints must be tested separately, since changing/deleting the
+ // HTTP password breaks all further calls.
+ // See tests updateHttpPasswordEndpoints and deleteHttpPasswordEndpoints.
+
RestCall.get("/accounts/%s/preferences"),
RestCall.put("/accounts/%s/preferences"),
RestCall.get("/accounts/%s/preferences.diff"),
RestCall.put("/accounts/%s/preferences.diff"),
RestCall.get("/accounts/%s/preferences.edit"),
RestCall.put("/accounts/%s/preferences.edit"),
+ RestCall.get("/accounts/%s/sshkeys/"),
+ RestCall.post("/accounts/%s/sshkeys/"),
RestCall.get("/accounts/%s/starred.changes"),
- RestCall.post("/accounts/%s/index"),
- RestCall.get("/accounts/%s/agreements"),
- RestCall.put("/accounts/%s/agreements"),
- RestCall.get("/accounts/%s/external.ids"),
- RestCall.post("/accounts/%s/external.ids:delete"),
- RestCall.post("/accounts/%s/drafts:delete"),
- RestCall.get("/accounts/%s/oauthtoken"),
- RestCall.get("/accounts/%s/capabilities"),
- RestCall.get("/accounts/%s/capabilities/viewPlugins"),
- RestCall.get("/accounts/%s/gpgkeys"),
- RestCall.post("/accounts/%s/gpgkeys"),
+ RestCall.get("/accounts/%s/status"),
+ RestCall.put("/accounts/%s/status"),
+ RestCall.get("/accounts/%s/username"),
+ // Changing the username is not allowed.
+ RestCall.builder(PUT, "/accounts/%s/username")
+ .expectedResponseCode(SC_METHOD_NOT_ALLOWED)
+ .expectedMessage("Username cannot be changed.")
+ .build(),
+ RestCall.get("/accounts/%s/watched.projects"),
+ RestCall.post("/accounts/%s/watched.projects"),
+ RestCall.post("/accounts/%s/watched.projects:delete"),
+
// Account deletion must be the last tested endpoint
RestCall.delete("/accounts/%s"));
@@ -146,11 +164,27 @@
RestCall.delete("/accounts/%s/starred.changes/%s"));
@Test
+ @GerritConfigs(
+ value = {
+ @GerritConfig(name = "auth.contributorAgreements", value = "true"),
+ @GerritConfig(name = "auth.registerEmailPrivateKey", value = "KEY"),
+ @GerritConfig(name = "receive.enableSignedPush", value = "true"),
+ })
public void accountEndpoints() throws Exception {
execute(adminRestSession, ACCOUNT_ENDPOINTS, "self");
}
@Test
+ public void updateHttpPasswordEndpoints() throws Exception {
+ execute(adminRestSession, RestCall.put("/accounts/%s/password.http"), "self");
+ }
+
+ @Test
+ public void deleteHttpPasswordEndpoints() throws Exception {
+ execute(adminRestSession, RestCall.delete("/accounts/%s/password.http"), "self");
+ }
+
+ @Test
public void emailEndpoints() throws Exception {
execute(adminRestSession, EMAIL_ENDPOINTS, "self", admin.email());
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 2df820b..6a5441c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -54,54 +54,57 @@
private static final ImmutableList<RestCall> CHANGE_ENDPOINTS =
ImmutableList.of(
RestCall.get("/changes/%s"),
- RestCall.get("/changes/%s/detail"),
- RestCall.get("/changes/%s/topic"),
- RestCall.put("/changes/%s/topic"),
- RestCall.delete("/changes/%s/topic"),
- RestCall.get("/changes/%s/in"),
- RestCall.get("/changes/%s/hashtags"),
- RestCall.get("/changes/%s/custom_keyed_values"),
- RestCall.get("/changes/%s/comments"),
- RestCall.get("/changes/%s/robotcomments"),
- RestCall.get("/changes/%s/drafts"),
+ RestCall.post("/changes/%s/abandon"),
RestCall.get("/changes/%s/attention"),
RestCall.post("/changes/%s/attention"),
+ RestCall.get("/changes/%s/check"),
+ RestCall.post("/changes/%s/check"),
+ RestCall.post("/changes/%s/check.submit_requirement"),
+ RestCall.get("/changes/%s/comments"),
+ RestCall.get("/changes/%s/custom_keyed_values"),
+ RestCall.post("/changes/%s/custom_keyed_values"),
+ RestCall.get("/changes/%s/detail"),
+ RestCall.get("/changes/%s/drafts"),
+ RestCall.get("/changes/%s/edit"),
+ RestCall.post("/changes/%s/edit"),
+ RestCall.put("/changes/%s/edit/a.txt"),
+ RestCall.get("/changes/%s/edit:message"),
+ RestCall.put("/changes/%s/edit:message"),
+ RestCall.post("/changes/%s/edit:publish"),
+ RestCall.post("/changes/%s/edit:rebase"),
+ RestCall.get("/changes/%s/hashtags"),
+ RestCall.get("/changes/%s/in"),
+ RestCall.post("/changes/%s/index"),
+ RestCall.get("/changes/%s/meta_diff"),
+ RestCall.post("/changes/%s/merge"),
+ RestCall.get("/changes/%s/messages"),
+ RestCall.put("/changes/%s/message"),
+ RestCall.post("/changes/%s/move"),
+ RestCall.post("/changes/%s/patch:apply"),
RestCall.post("/changes/%s/private"),
RestCall.post("/changes/%s/private.delete"),
RestCall.delete("/changes/%s/private"),
- RestCall.post("/changes/%s/wip"),
+ RestCall.get("/changes/%s/pure_revert"),
RestCall.post("/changes/%s/ready"),
- RestCall.get("/changes/%s/messages"),
- RestCall.put("/changes/%s/message"),
- RestCall.post("/changes/%s/merge"),
- RestCall.post("/changes/%s/abandon"),
- RestCall.post("/changes/%s/move"),
RestCall.post("/changes/%s/rebase"),
+ RestCall.post("/changes/%s/rebase:chain"),
RestCall.post("/changes/%s/restore"),
RestCall.post("/changes/%s/revert"),
RestCall.post("/changes/%s/revert_submission"),
- RestCall.get("/changes/%s/pure_revert"),
- RestCall.post("/changes/%s/submit"),
- RestCall.get("/changes/%s/submitted_together"),
- RestCall.post("/changes/%s/index"),
- RestCall.get("/changes/%s/check"),
- RestCall.post("/changes/%s/check"),
RestCall.get("/changes/%s/reviewers"),
RestCall.post("/changes/%s/reviewers"),
+ // GET /changes/<change-id>/revisions is not implemented
+ RestCall.builder(GET, "/changes/%s/revisions").expectedResponseCode(SC_NOT_FOUND).build(),
+ RestCall.get("/changes/%s/robotcomments"),
+ RestCall.get("/changes/%s/topic"),
+ RestCall.put("/changes/%s/topic"),
+ RestCall.delete("/changes/%s/topic"),
+ RestCall.post("/changes/%s/submit"),
+ RestCall.get("/changes/%s/submitted_together"),
RestCall.get("/changes/%s/suggest_reviewers"),
- RestCall.builder(GET, "/changes/%s/revisions")
- // GET /changes/<change-id>/revisions is not implemented
- .expectedResponseCode(SC_NOT_FOUND)
- .build(),
- RestCall.get("/changes/%s/edit"),
- RestCall.post("/changes/%s/edit"),
- RestCall.post("/changes/%s/edit:rebase"),
- RestCall.get("/changes/%s/edit:message"),
- RestCall.put("/changes/%s/edit:message"),
-
- // Publish edit and create a new edit
- RestCall.post("/changes/%s/edit:publish"),
- RestCall.put("/changes/%s/edit/a.txt"),
+ // GET /changes/<change-id>/votes is not implemented
+ RestCall.builder(GET, "/changes/%s/votes").expectedResponseCode(SC_NOT_FOUND).build(),
+ RestCall.post("/changes/%s/wip"),
// Deletion of change edit and change must be tested last
RestCall.delete("/changes/%s/edit"),
@@ -114,9 +117,9 @@
private static final ImmutableList<RestCall> REVIEWER_ENDPOINTS =
ImmutableList.of(
RestCall.get("/changes/%s/reviewers/%s"),
- RestCall.get("/changes/%s/reviewers/%s/votes"),
+ RestCall.delete("/changes/%s/reviewers/%s"),
RestCall.post("/changes/%s/reviewers/%s/delete"),
- RestCall.delete("/changes/%s/reviewers/%s"));
+ RestCall.get("/changes/%s/reviewers/%s/votes"));
/**
* Vote REST endpoints to be tested, each URL contains placeholders for the change identifier, the
@@ -134,36 +137,36 @@
private static final ImmutableList<RestCall> REVISION_ENDPOINTS =
ImmutableList.of(
RestCall.get("/changes/%s/revisions/%s/actions"),
+ RestCall.get("/changes/%s/revisions/%s/archive"),
RestCall.post("/changes/%s/revisions/%s/cherrypick"),
+ RestCall.get("/changes/%s/revisions/%s/comments"),
RestCall.get("/changes/%s/revisions/%s/commit"),
+ RestCall.get("/changes/%s/revisions/%s/description"),
+ RestCall.put("/changes/%s/revisions/%s/description"),
+ RestCall.get("/changes/%s/revisions/%s/drafts"),
+ RestCall.put("/changes/%s/revisions/%s/drafts"),
+ RestCall.get("/changes/%s/revisions/%s/files"),
+ // GET /changes/<change>/revisions/<revision>/fixes is not implemented
+ RestCall.builder(GET, "/changes/%s/revisions/%s/fixes")
+ .expectedResponseCode(SC_NOT_FOUND)
+ .build(),
+ RestCall.post("/changes/%s/revisions/%s/fix:apply"),
+ RestCall.post("/changes/%s/revisions/%s/fix:preview"),
RestCall.get("/changes/%s/revisions/%s/mergeable"),
+ RestCall.get("/changes/%s/revisions/%s/mergelist"),
+ RestCall.get("/changes/%s/revisions/%s/patch"),
+ RestCall.get("/changes/%s/revisions/%s/ported_comments"),
+ RestCall.get("/changes/%s/revisions/%s/ported_drafts"),
+ RestCall.post("/changes/%s/revisions/%s/rebase"),
RestCall.get("/changes/%s/revisions/%s/related"),
RestCall.get("/changes/%s/revisions/%s/review"),
RestCall.post("/changes/%s/revisions/%s/review"),
+ RestCall.get("/changes/%s/revisions/%s/reviewers"),
+ RestCall.get("/changes/%s/revisions/%s/robotcomments"),
RestCall.post("/changes/%s/revisions/%s/submit"),
RestCall.get("/changes/%s/revisions/%s/submit_type"),
RestCall.post("/changes/%s/revisions/%s/test.submit_rule"),
- RestCall.post("/changes/%s/revisions/%s/test.submit_type"),
- RestCall.post("/changes/%s/revisions/%s/rebase"),
- RestCall.post("/changes/%s/revisions/%s/fix:apply"),
- RestCall.post("/changes/%s/revisions/%s/fix:preview"),
- RestCall.get("/changes/%s/revisions/%s/description"),
- RestCall.put("/changes/%s/revisions/%s/description"),
- RestCall.get("/changes/%s/revisions/%s/patch"),
- RestCall.get("/changes/%s/revisions/%s/archive"),
- RestCall.get("/changes/%s/revisions/%s/mergelist"),
- RestCall.get("/changes/%s/revisions/%s/reviewers"),
- RestCall.get("/changes/%s/revisions/%s/drafts"),
- RestCall.put("/changes/%s/revisions/%s/drafts"),
- RestCall.get("/changes/%s/revisions/%s/comments"),
- RestCall.get("/changes/%s/revisions/%s/robotcomments"),
- RestCall.get("/changes/%s/revisions/%s/ported_comments"),
- RestCall.get("/changes/%s/revisions/%s/ported_drafts"),
- RestCall.builder(GET, "/changes/%s/revisions/%s/fixes")
- // GET /changes/<change>/revisions/<revision>/fixes is not implemented
- .expectedResponseCode(SC_NOT_FOUND)
- .build(),
- RestCall.get("/changes/%s/revisions/%s/files"));
+ RestCall.post("/changes/%s/revisions/%s/test.submit_type"));
/**
* Revision reviewer REST endpoints to be tested, each URL contains placeholders for the change
@@ -172,8 +175,8 @@
private static final ImmutableList<RestCall> REVISION_REVIEWER_ENDPOINTS =
ImmutableList.of(
RestCall.get("/changes/%s/revisions/%s/reviewers/%s"),
- RestCall.get("/changes/%s/revisions/%s/reviewers/%s/votes"),
RestCall.post("/changes/%s/revisions/%s/reviewers/%s/delete"),
+ RestCall.get("/changes/%s/revisions/%s/reviewers/%s/votes"),
RestCall.delete("/changes/%s/revisions/%s/reviewers/%s"));
/**
@@ -225,12 +228,12 @@
*/
private static final ImmutableList<RestCall> REVISION_FILE_ENDPOINTS =
ImmutableList.of(
- RestCall.put("/changes/%s/revisions/%s/files/%s/reviewed"),
- RestCall.delete("/changes/%s/revisions/%s/files/%s/reviewed"),
+ RestCall.get("/changes/%s/revisions/%s/files/%s/blame"),
RestCall.get("/changes/%s/revisions/%s/files/%s/content"),
- RestCall.get("/changes/%s/revisions/%s/files/%s/download"),
RestCall.get("/changes/%s/revisions/%s/files/%s/diff"),
- RestCall.get("/changes/%s/revisions/%s/files/%s/blame"));
+ RestCall.get("/changes/%s/revisions/%s/files/%s/download"),
+ RestCall.put("/changes/%s/revisions/%s/files/%s/reviewed"),
+ RestCall.delete("/changes/%s/revisions/%s/files/%s/reviewed"));
/**
* Change message REST endpoints to be tested, each URL contains placeholders for the change
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
index 00dcb4f..57279d3 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
@@ -46,7 +46,12 @@
*/
private static final ImmutableList<RestCall> CONFIG_ENDPOINTS =
ImmutableList.of(
- RestCall.get("/config/server/version"),
+ RestCall.get("/config/server/caches"),
+ RestCall.post("/config/server/caches"),
+ RestCall.get("/config/server/capabilities"),
+ RestCall.post("/config/server/check.consistency"),
+ RestCall.put("/config/server/email.confirm"),
+ RestCall.post("/config/server/index.changes"),
RestCall.get("/config/server/info"),
RestCall.get("/config/server/preferences"),
RestCall.put("/config/server/preferences"),
@@ -54,28 +59,22 @@
RestCall.put("/config/server/preferences.diff"),
RestCall.get("/config/server/preferences.edit"),
RestCall.put("/config/server/preferences.edit"),
- RestCall.get("/config/server/top-menus"),
- RestCall.put("/config/server/email.confirm"),
- RestCall.post("/config/server/check.consistency"),
RestCall.post("/config/server/reload"),
RestCall.get("/config/server/summary"),
- RestCall.get("/config/server/capabilities"),
- RestCall.get("/config/server/caches"),
- RestCall.post("/config/server/caches"),
RestCall.get("/config/server/tasks"),
- RestCall.post("/config/server/index.changes"));
+ RestCall.get("/config/server/top-menus"),
+ RestCall.get("/config/server/version"));
/**
* Cache REST endpoints to be tested, the URLs contain a placeholder for the cache identifier.
- * Since there is only supported a single supported config identifier ('server') it can be
- * hard-coded.
+ * Since there is only a single supported config identifier ('server') it can be hard-coded.
*/
private static final ImmutableList<RestCall> CACHE_ENDPOINTS =
ImmutableList.of(RestCall.get("/config/server/caches/%s"));
/**
* Task REST endpoints to be tested, the URLs contain a placeholder for the task identifier. Since
- * there is only supported a single supported config identifier ('server') it can be hard-coded.
+ * there is only a single supported config identifier ('server') it can be hard-coded.
*/
private static final ImmutableList<RestCall> TASK_ENDPOINTS =
ImmutableList.of(
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
index bb12172..74241d8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
@@ -34,26 +34,26 @@
ImmutableList.of(
RestCall.get("/groups/%s"),
RestCall.put("/groups/%s"),
- RestCall.get("/groups/%s/detail"),
- RestCall.get("/groups/%s/name"),
- RestCall.put("/groups/%s/name"),
RestCall.get("/groups/%s/description"),
RestCall.put("/groups/%s/description"),
RestCall.delete("/groups/%s/description"),
- RestCall.get("/groups/%s/owner"),
- RestCall.put("/groups/%s/owner"),
- RestCall.get("/groups/%s/options"),
- RestCall.put("/groups/%s/options"),
- RestCall.post("/groups/%s/members"),
- RestCall.post("/groups/%s/members.add"),
- RestCall.post("/groups/%s/members.delete"),
+ RestCall.get("/groups/%s/detail"),
+ RestCall.get("/groups/%s/groups"),
RestCall.post("/groups/%s/groups"),
RestCall.post("/groups/%s/groups.add"),
RestCall.post("/groups/%s/groups.delete"),
- RestCall.get("/groups/%s/log.audit"),
RestCall.post("/groups/%s/index"),
+ RestCall.get("/groups/%s/log.audit"),
RestCall.get("/groups/%s/members"),
- RestCall.get("/groups/%s/groups"));
+ RestCall.post("/groups/%s/members"),
+ RestCall.post("/groups/%s/members.add"),
+ RestCall.post("/groups/%s/members.delete"),
+ RestCall.get("/groups/%s/name"),
+ RestCall.put("/groups/%s/name"),
+ RestCall.get("/groups/%s/options"),
+ RestCall.put("/groups/%s/options"),
+ RestCall.get("/groups/%s/owner"),
+ RestCall.put("/groups/%s/owner"));
/**
* Member REST endpoints to be tested, each URL contains placeholders for the group identifier and
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ListProjectOptionsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ListProjectOptionsRestApiBindingsIT.java
new file mode 100644
index 0000000..1d209e1
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ListProjectOptionsRestApiBindingsIT.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2023 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.acceptance.rest.binding;
+
+import static com.google.gerrit.acceptance.rest.util.RestCall.Method.GET;
+import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.http.HttpStatus.SC_OK;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import org.junit.Test;
+
+public class ListProjectOptionsRestApiBindingsIT extends AbstractDaemonTest {
+ private static final ImmutableList<RestCall> LIST_PROJECTS_WITH_OPTIONS =
+ ImmutableList.of(
+ // =========================
+ // === Supported options ===
+ // =========================
+ get200OK("/projects/?show-branch=refs/heads/master"),
+ get200OK("/projects/?b=refs/heads/master"),
+ get200OK("/projects/?format=TEXT"),
+ get200OK("/projects/?format=JSON"),
+ get200OK("/projects/?format=JSON_COMPACT"),
+ get200OK("/projects/?tree"),
+ get200OK("/projects/?tree=true"),
+ get200OK("/projects/?tree=false"),
+ get200OK("/projects/?t"),
+ get200OK("/projects/?t=true"),
+ get200OK("/projects/?t=false"),
+ get200OK("/projects/?type=ALL"),
+ get200OK("/projects/?type=CODE"),
+ get200OK("/projects/?type=PERMISSIONS"),
+ get200OK("/projects/?description"),
+ get200OK("/projects/?description=true"),
+ get200OK("/projects/?description=false"),
+ get200OK("/projects/?d"),
+ get200OK("/projects/?d=true"),
+ get200OK("/projects/?d=false"),
+ get200OK("/projects/?all"),
+ get200OK("/projects/?all=true"),
+ get200OK("/projects/?all=false"),
+ get200OK("/projects/?state=ACTIVE"),
+ get200OK("/projects/?state=READ_ONLY"),
+ get200OK("/projects/?state=HIDDEN"),
+ get200OK("/projects/?limit=10"),
+ get200OK("/projects/?n=10"),
+ get200OK("/projects/?start=10"),
+ get200OK("/projects/?S=10"),
+ get200OK("/projects/?prefix=my-prefix"),
+ get200OK("/projects/?p=my-prefix"),
+ get200OK("/projects/?match=my-match"),
+ get200OK("/projects/?m=my-match"),
+ get200OK("/projects/?r=my-regex"),
+ get200OK("/projects/?has-acl-for=" + SystemGroupBackend.ANONYMOUS_USERS.get()),
+
+ // ===========================
+ // === Unsupported options ===
+ // ===========================
+ get400BadRequest("/projects/?unknown", "\"--unknown\" is not a valid option"),
+ get400BadRequest("/projects/?unknown", "\"--unknown\" is not a valid option"),
+ get400BadRequest(
+ "/projects/?format=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"--format\""),
+ get400BadRequest("/projects/?tree=UNKNOWN", "invalid boolean \"tree=UNKNOWN\""),
+ get400BadRequest("/projects/?t=UNKNOWN", "invalid boolean \"t=UNKNOWN\""),
+ get400BadRequest(
+ "/projects/?type=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"--type\""),
+ get400BadRequest(
+ "/projects/?description=UNKNOWN", "invalid boolean \"description=UNKNOWN\""),
+ get400BadRequest("/projects/?d=UNKNOWN", "invalid boolean \"d=UNKNOWN\""),
+ get400BadRequest("/projects/?all=UNKNOWN", "invalid boolean \"all=UNKNOWN\""),
+ get400BadRequest(
+ "/projects/?state=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"--state\""),
+ get400BadRequest("/projects/?n=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"-n\""),
+ get400BadRequest(
+ "/projects/?start=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"--start\""),
+ get400BadRequest("/projects/?S=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"-S\""),
+ get400BadRequest("/projects/?has-acl-for=UNKNOWN", "Group \"UNKNOWN\" does not exist"));
+
+ private static RestCall get200OK(String uriFormat) {
+ return RestCall.builder(GET, uriFormat).expectedResponseCode(SC_OK).build();
+ }
+
+ private static RestCall get400BadRequest(String uriFormat, String expectedMessage) {
+ return RestCall.builder(GET, uriFormat)
+ .expectedResponseCode(SC_BAD_REQUEST)
+ .expectedMessage(expectedMessage)
+ .build();
+ }
+
+ @Test
+ public void listProjectsWithOptions() throws Exception {
+ RestApiCallHelper.execute(adminRestSession, LIST_PROJECTS_WITH_OPTIONS);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index cffcc2f..43d456e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -55,41 +55,42 @@
ImmutableList.of(
RestCall.get("/projects/%s"),
RestCall.put("/projects/%s"),
- RestCall.get("/projects/%s/description"),
- RestCall.put("/projects/%s/description"),
- RestCall.delete("/projects/%s/description"),
- RestCall.get("/projects/%s/parent"),
- RestCall.put("/projects/%s/parent"),
- RestCall.get("/projects/%s/config"),
- RestCall.put("/projects/%s/config"),
- RestCall.get("/projects/%s/HEAD"),
- RestCall.put("/projects/%s/HEAD"),
RestCall.get("/projects/%s/access"),
RestCall.post("/projects/%s/access"),
RestCall.put("/projects/%s/access:review"),
- RestCall.get("/projects/%s/check.access"),
RestCall.put("/projects/%s/ban"),
- RestCall.get("/projects/%s/statistics.git"),
- RestCall.post("/projects/%s/index"),
- RestCall.post("/projects/%s/gc"),
- RestCall.post("/projects/%s/create.change"),
- RestCall.get("/projects/%s/children"),
RestCall.get("/projects/%s/branches"),
- RestCall.post("/projects/%s/branches:delete"),
RestCall.put("/projects/%s/branches/new-branch"),
- RestCall.get("/projects/%s/labels"),
- RestCall.get("/projects/%s/tags"),
- RestCall.post("/projects/%s/tags:delete"),
- RestCall.put("/projects/%s/tags/new-tag"),
- RestCall.builder(GET, "/projects/%s/commits")
- // GET /projects/<project>/branches/<branch>/commits is not implemented
- .expectedResponseCode(SC_NOT_FOUND)
- .build(),
+ RestCall.post("/projects/%s/branches:delete"),
+ RestCall.post("/projects/%s/check"),
+ RestCall.get("/projects/%s/check.access"),
+ RestCall.get("/projects/%s/children"),
+ // GET /projects/<project>/branches/<branch>/commits is not implemented
+ RestCall.builder(GET, "/projects/%s/commits").expectedResponseCode(SC_NOT_FOUND).build(),
+ RestCall.get("/projects/%s/commits:in"),
+ RestCall.get("/projects/%s/config"),
+ RestCall.put("/projects/%s/config"),
+ RestCall.post("/projects/%s/create.change"),
RestCall.get("/projects/%s/dashboards"),
- RestCall.put("/projects/%s/labels/new-label"),
+ RestCall.get("/projects/%s/description"),
+ RestCall.put("/projects/%s/description"),
+ RestCall.delete("/projects/%s/description"),
+ RestCall.post("/projects/%s/gc"),
+ RestCall.get("/projects/%s/HEAD"),
+ RestCall.put("/projects/%s/HEAD"),
+ RestCall.post("/projects/%s/index"),
+ RestCall.post("/projects/%s/index.changes"),
+ RestCall.get("/projects/%s/labels"),
RestCall.post("/projects/%s/labels/"),
+ RestCall.put("/projects/%s/labels/new-label"),
+ RestCall.get("/projects/%s/parent"),
+ RestCall.put("/projects/%s/parent"),
+ RestCall.get("/projects/%s/statistics.git"),
+ RestCall.get("/projects/%s/submit_requirements"),
RestCall.put("/projects/%s/submit_requirements/new-sr"),
- RestCall.get("/projects/%s/submit_requirements"));
+ RestCall.get("/projects/%s/tags"),
+ RestCall.put("/projects/%s/tags/new-tag"),
+ RestCall.post("/projects/%s/tags:delete"));
/**
* Child project REST endpoints to be tested, each URL contains placeholders for the parent
@@ -106,16 +107,16 @@
ImmutableList.of(
RestCall.get("/projects/%s/branches/%s"),
RestCall.put("/projects/%s/branches/%s"),
+ // GET /projects/<project>/branches/<branch>/files is not implemented
+ RestCall.builder(GET, "/projects/%s/branches/%s/files")
+ .expectedResponseCode(SC_NOT_FOUND)
+ .build(),
RestCall.get("/projects/%s/branches/%s/mergeable"),
+ // The tests use DfsRepository which does not support getting the reflog.
RestCall.builder(GET, "/projects/%s/branches/%s/reflog")
- // The tests use DfsRepository which does not support getting the reflog.
.expectedResponseCode(SC_METHOD_NOT_ALLOWED)
.expectedMessage("reflog not supported on")
.build(),
- RestCall.builder(GET, "/projects/%s/branches/%s/files")
- // GET /projects/<project>/branches/<branch>/files is not implemented
- .expectedResponseCode(SC_NOT_FOUND)
- .build(),
// Branch deletion must be tested last
RestCall.delete("/projects/%s/branches/%s"));
@@ -156,9 +157,9 @@
private static final ImmutableList<RestCall> COMMIT_ENDPOINTS =
ImmutableList.of(
RestCall.get("/projects/%s/commits/%s"),
- RestCall.get("/projects/%s/commits/%s/in"),
+ RestCall.post("/projects/%s/commits/%s/cherrypick"),
RestCall.get("/projects/%s/commits/%s/files"),
- RestCall.post("/projects/%s/commits/%s/cherrypick"));
+ RestCall.get("/projects/%s/commits/%s/in"));
/**
* Commit file REST endpoints to be tested, each URL contains placeholders for the project
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 0550cb9..f8eccb5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -44,6 +44,7 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.UseSystemTime;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.BranchNameKey;
@@ -192,6 +193,16 @@
}
@Test
+ @GerritConfig(name = "change.topicLimit", value = "3")
+ public void createNewChange_exceedsTopicLimit() throws Exception {
+ assertCreateSucceeds(newChangeWithTopic("limited"));
+ assertCreateSucceeds(newChangeWithTopic("limited"));
+ assertCreateSucceeds(newChangeWithTopic("limited"));
+ ChangeInput ci = newChangeWithTopic("limited");
+ assertCreateFails(ci, BadRequestException.class, "topicLimit");
+ }
+
+ @Test
public void createNewChange() throws Exception {
ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
assertThat(info.revisions.get(info.currentRevision).commit.message)
@@ -1319,6 +1330,12 @@
return in;
}
+ private ChangeInput newChangeWithTopic(String topic) {
+ ChangeInput in = newChangeInput(ChangeStatus.NEW);
+ in.topic = topic;
+ return in;
+ }
+
private ChangeInfo assertCreateSucceeds(ChangeInput in) throws Exception {
ChangeInfo out = gApi.changes().create(in).get();
validateCreateSucceeds(in, out);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CustomKeyedValuesIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CustomKeyedValuesIT.java
index 03722e6..3a1d909 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CustomKeyedValuesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CustomKeyedValuesIT.java
@@ -27,9 +27,8 @@
import com.google.common.collect.Iterables;
import com.google.common.truth.MapSubject;
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.UseClockStep;
+import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.extensions.api.changes.CustomKeyedValuesInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -38,19 +37,29 @@
import com.google.inject.Inject;
import org.junit.Test;
-@NoHttpd
-@UseClockStep
public class CustomKeyedValuesIT extends AbstractDaemonTest {
@Inject private RequestScopeOperations requestScopeOperations;
@Test
public void getNoCustomKeyedValues() throws Exception {
- // Get on a change with no hashtags returns an empty list.
+ // Get on a change with no custom keyed values returns an empty list.
PushOneCommit.Result r = createChange();
assertThatGet(r).isEmpty();
}
@Test
+ public void parsesInputCorrectly() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String endpoint = "/changes/" + r.getChangeId() + "/custom_keyed_values";
+ CustomKeyedValuesInput input = new CustomKeyedValuesInput();
+ input.add = ImmutableMap.of("key", "value");
+ RestResponse response = adminRestSession.post(endpoint, input);
+ response.assertOK();
+
+ assertThatGet(r).containsExactly("key", "value");
+ }
+
+ @Test
public void addSingleCustomKeyedValue() throws Exception {
PushOneCommit.Result r = createChange();
ChangeMessageInfo last = getLastMessage(r);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index c712b14..73e0d17 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -16,8 +16,11 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.util.Comparator.comparing;
import com.google.common.collect.Iterables;
@@ -25,13 +28,20 @@
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -39,6 +49,7 @@
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.server.git.ChangeMessageModifier;
+import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.submit.CommitMergeStatus;
import com.google.inject.Inject;
import java.util.List;
@@ -49,6 +60,9 @@
public class SubmitByCherryPickIT extends AbstractSubmit {
@Inject private ProjectOperations projectOperations;
@Inject private ExtensionRegistry extensionRegistry;
+ @Inject private AccountOperations accountOperations;
+ @Inject private ChangeOperations changeOperations;
+ @Inject private RequestScopeOperations requestScopeOperations;
@Override
protected SubmitType getSubmitType() {
@@ -94,6 +108,46 @@
}
@Test
+ public void submitWithCherryPickAfterUpdatingPreferredEmail() throws Throwable {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Add permissions to apply label "Code-Review": 2 and submit
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel(TestLabels.codeReview().getName())
+ .ref("refs/heads/master")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(allow(Permission.SUBMIT).ref("refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
+ // Create change to submit
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file("file")
+ .content("content")
+ .owner(testUser)
+ .create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Approve and submit the change
+ RevisionApi revision = gApi.changes().id(changeId.get()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+ assertThat(gApi.changes().id(changeId.get()).get().getCurrentRevision().commit.committer.email)
+ .isEqualTo(emailTwo);
+ }
+
+ @Test
public void changeMessageOnSubmit() throws Throwable {
PushOneCommit.Result change = createChange();
ChangeMessageModifier link =
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index d58ad11..a03a5b5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -16,7 +16,10 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.util.Comparator.comparing;
@@ -27,20 +30,28 @@
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.exceptions.MergeUpdateException;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.git.ChangeMessageModifier;
+import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.ObjectId;
@@ -51,6 +62,9 @@
@Inject private DynamicItem<UrlFormatter> urlFormatter;
@Inject private ProjectOperations projectOperations;
@Inject private ExtensionRegistry extensionRegistry;
+ @Inject private AccountOperations accountOperations;
+ @Inject private ChangeOperations changeOperations;
+ @Inject private RequestScopeOperations requestScopeOperations;
@Override
protected SubmitType getSubmitType() {
@@ -96,6 +110,46 @@
}
@Test
+ public void submitByRebaseAfterUpdatingPreferredEmail() throws Throwable {
+ String emailOne = "email1@example.com";
+ Account.Id testUser = accountOperations.newAccount().preferredEmail(emailOne).create();
+
+ // Add permissions to apply label "Code-Review": 2 and submit
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel(TestLabels.codeReview().getName())
+ .ref("refs/heads/master")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(allow(Permission.SUBMIT).ref("refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
+ // Create change to submit
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .project(project)
+ .file("file")
+ .content("content")
+ .owner(testUser)
+ .create();
+
+ // Change preferred email for the user
+ String emailTwo = "email2@example.com";
+ accountOperations.account(testUser).forUpdate().preferredEmail(emailTwo).update();
+ requestScopeOperations.setApiUser(testUser);
+
+ // Approve and submit the change
+ RevisionApi revision = gApi.changes().id(changeId.get()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+ assertThat(gApi.changes().id(changeId.get()).get().getCurrentRevision().commit.committer.email)
+ .isEqualTo(emailTwo);
+ }
+
+ @Test
public void rebaseInvokesChangeMessageModifiers() throws Throwable {
ChangeMessageModifier modifier1 =
(msg, orig, tip, dest) -> msg + "This-change-before-rebase: " + orig.name() + "\n";
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index e69f781..1df3f3e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -43,7 +43,7 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.project.ProjectCacheImpl;
-import com.google.gerrit.server.restapi.project.ListProjects;
+import com.google.gerrit.server.restapi.project.ListProjectsImpl;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.inject.Inject;
@@ -59,7 +59,7 @@
public class ListProjectsIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
- @Inject private ListProjects listProjects;
+ @Inject private ListProjectsImpl listProjects;
@Test
public void listProjects() throws Exception {
@@ -140,6 +140,33 @@
}
@Test
+ @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "true")
+ public void listProjectsSetsMoreProjectsIfLimited_indexEnabled() throws Exception {
+ testListProjectsSetsMoreProjectsIfLimited();
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "false")
+ public void listProjectsSetsMoreProjectsIfLimited_indexDisabled() throws Exception {
+ testListProjectsSetsMoreProjectsIfLimited();
+ }
+
+ private void testListProjectsSetsMoreProjectsIfLimited() throws Exception {
+ for (int i = 0; i < 3; i++) {
+ projectOperations.newProject().name("prefix-" + i).create();
+ }
+
+ List<ProjectInfo> result = gApi.projects().list().withPrefix("prefix").get();
+ assertThat(Iterables.getLast(result)._moreProjects).isNull();
+
+ result = gApi.projects().list().withPrefix("prefix").withLimit(Integer.MAX_VALUE).get();
+ assertThat(Iterables.getLast(result)._moreProjects).isNull();
+
+ result = gApi.projects().list().withPrefix("prefix").withLimit(2).get();
+ assertThat(Iterables.getLast(result)._moreProjects).isTrue();
+ }
+
+ @Test
public void listProjectsToOutputStream() throws Exception {
int numInitialProjects = gApi.projects().list().get().size();
int numTestProjects = 5;
@@ -159,7 +186,8 @@
@Test
public void listProjectsAsJsonMultilineToOutputStream() throws Exception {
- listProjectsAsJsonToOutputStream(OutputFormat.JSON);
+ String jsonOutput = listProjectsAsJsonToOutputStream(OutputFormat.JSON);
+ assertThat(jsonOutput).contains("\n");
}
@Test
@@ -198,7 +226,18 @@
}
@Test
- public void listProjectsWithPrefix() throws Exception {
+ @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "true")
+ public void listProjectsWithPrefix_indexEnabled() throws Exception {
+ testListProjectsWithPrefix();
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "false")
+ public void listProjectsWithPrefix_indexDisabled() throws Exception {
+ testListProjectsWithPrefix();
+ }
+
+ private void testListProjectsWithPrefix() throws Exception {
Project.NameKey someProject = projectOperations.newProject().name("listtest-p1").create();
Project.NameKey someOtherProject = projectOperations.newProject().name("listtest-p2").create();
projectOperations.newProject().name("other-prefix-project").create();
@@ -248,7 +287,18 @@
}
@Test
- public void listProjectsWithSubstring() throws Exception {
+ @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "true")
+ public void listProjectsWithSubstring_indexEnabled() throws Exception {
+ testListProjectsWithSubstring();
+ }
+
+ @Test
+ @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "false")
+ public void listProjectsWithSubstring_indexDisabled() throws Exception {
+ testListProjectsWithSubstring();
+ }
+
+ private void testListProjectsWithSubstring() throws Exception {
Project.NameKey someProject = projectOperations.newProject().name("some-project").create();
Project.NameKey someOtherProject =
projectOperations.newProject().name("some-other-project").create();
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
index 55735fc..3eb6eb2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
+++ b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
@@ -19,6 +19,7 @@
import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.RestResponse;
@@ -92,7 +93,8 @@
} else {
assertWithMessage(msg)
.that(status)
- .isNotIn(ImmutableList.of(SC_FORBIDDEN, SC_NOT_FOUND, SC_METHOD_NOT_ALLOWED));
+ .isNotIn(
+ ImmutableList.of(SC_UNAUTHORIZED, SC_FORBIDDEN, SC_NOT_FOUND, SC_METHOD_NOT_ALLOWED));
assertWithMessage(msg).that(status).isLessThan(SC_INTERNAL_SERVER_ERROR);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensivityMigratorIT.java b/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensivityMigratorIT.java
index b569fe0..ee00e40 100644
--- a/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensivityMigratorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensivityMigratorIT.java
@@ -27,9 +27,9 @@
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.OnlineExternalIdCaseSensivityMigratiorExecutor;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdCaseSensitivityMigrator;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.storage.notedb.OnlineExternalIdCaseSensivityMigrator;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.inject.AbstractModule;
diff --git a/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java b/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
index 107b777..97024f2 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
@@ -100,13 +100,16 @@
// Run the cleanup logic. The zombie draft is cleared. The published comment is untouched.
DeleteZombieCommentsRefs worker =
deleteZombieDraftsFactory.create(/* cleanupPercentage= */ 100, dryRun);
- assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(1);
+ worker.setup();
+ assertThat(worker.listDraftCommentsThatAreAlsoPublished()).hasSize(1);
+ int deletedDrafts = worker.execute();
if (dryRun) {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).hasSize(1);
} else {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).isEmpty();
}
assertNumPublishedComments(changeId, 1);
+ assertThat(deletedDrafts).isEqualTo(1);
}
@Test
@@ -136,7 +139,9 @@
// Run the zombie cleanup logic. Zombie draft ref for PS2 will be removed.
DeleteZombieCommentsRefs worker =
deleteZombieDraftsFactory.create(/* cleanupPercentage= */ 100, dryRun);
- assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(1);
+ worker.setup();
+ assertThat(worker.listDraftCommentsThatAreAlsoPublished()).hasSize(1);
+ int deletedDrafts = worker.execute();
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r1.getCommit().name())).hasSize(1);
if (dryRun) {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).hasSize(1);
@@ -144,10 +149,13 @@
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).isEmpty();
}
assertNumPublishedComments(changeId, 1);
+ assertThat(deletedDrafts).isEqualTo(1);
// Re-run the worker: nothing happens.
- assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(dryRun ? 1 : 0);
+ assertThat(worker.listDraftCommentsThatAreAlsoPublished()).hasSize(dryRun ? 1 : 0);
+ deletedDrafts = worker.execute();
assertNumDrafts(changeId, 1);
+ assertThat(deletedDrafts).isEqualTo(dryRun ? 1 : 0);
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r1.getCommit().name())).hasSize(1);
if (dryRun) {
assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).hasSize(1);
diff --git a/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java b/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
index d01a81d..b8ec57b 100644
--- a/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
@@ -18,6 +18,8 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.server.config.GerritIsReplicaProvider;
import com.google.gerrit.testing.ConfigSuite;
import com.google.inject.Inject;
@@ -35,6 +37,14 @@
@Test
public void isNotReplica() {
assertThat(isReplicaProvider.get()).isFalse();
+ assertThat(server.isReplica()).isFalse();
+ }
+
+ @Test
+ @UseLocalDisk
+ public void isNotReplicaWithLocalDisk() {
+ assertThat(isReplicaProvider.get()).isFalse();
+ assertThat(server.isReplica()).isFalse();
}
@Test
@@ -42,5 +52,30 @@
public void isReplica() throws Exception {
restartAsSlave();
assertThat(isReplicaProvider.get()).isTrue();
+ assertThat(server.isReplica()).isTrue();
+ }
+
+ @Test
+ @GerritConfig(name = "container.replica", value = "true")
+ public void isReplicaFromGerritConfigAnnotation() throws Exception {
+ assertThat(isReplicaProvider.get()).isTrue();
+ assertThat(server.isReplica()).isTrue();
+ }
+
+ @Test
+ @UseLocalDisk
+ @GerritConfig(name = "container.replica", value = "true")
+ public void isReplicaWithLocalDisk() throws Exception {
+ assertThat(isReplicaProvider.get()).isTrue();
+ assertThat(server.isReplica()).isTrue();
+ }
+
+ @Test
+ @Sandboxed
+ @UseLocalDisk
+ public void isReplicaWithLocalDiskAfterRestart() throws Exception {
+ restartAsSlave();
+ assertThat(isReplicaProvider.get()).isTrue();
+ assertThat(server.isReplica()).isTrue();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java
index 1b164eb..f4dc798 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java
@@ -27,8 +27,8 @@
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
-import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.notedb.Sequences;
import com.google.inject.AbstractModule;
diff --git a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
index 04f9827..0389c4f 100644
--- a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
+++ b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
@@ -38,6 +38,7 @@
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
import com.google.gerrit.server.account.externalids.PasswordVerifier;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
@@ -100,7 +101,7 @@
res = new FakeHttpServletResponse();
extIdKeyFactory = new ExternalIdKeyFactory(new ExternalIdKeyFactory.ConfigImpl(authConfig));
- extIdFactory = new ExternalIdFactory(extIdKeyFactory, authConfig);
+ extIdFactory = new ExternalIdFactoryNoteDbImpl(extIdKeyFactory, authConfig);
authRequestFactory = new AuthRequest.Factory(extIdKeyFactory);
pwdVerifier = new PasswordVerifier(extIdKeyFactory, authConfig);
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index cc1ee00..5cb7feb 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -115,9 +115,7 @@
when(gerritApi.config()).thenReturn(configApi);
assertThat(dynamicTemplateData(gerritApi, "/c/project/+/123"))
- .containsAtLeast(
- "defaultChangeDetailHex", "1916314",
- "changeRequestsPath", "changes/project~123");
+ .containsAtLeast("changeRequestsPath", "changes/project~123");
}
private static SanitizedContent ordain(String s) {
diff --git a/javatests/com/google/gerrit/metrics/BUILD b/javatests/com/google/gerrit/metrics/BUILD
new file mode 100644
index 0000000..531280e
--- /dev/null
+++ b/javatests/com/google/gerrit/metrics/BUILD
@@ -0,0 +1,13 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+ name = "field_tests",
+ size = "small",
+ srcs = glob(["*.java"]),
+ tags = ["metrics"],
+ deps = [
+ "//java/com/google/gerrit/metrics",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib/truth",
+ ],
+)
diff --git a/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java b/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java
new file mode 100644
index 0000000..f12b1b3
--- /dev/null
+++ b/javatests/com/google/gerrit/metrics/FieldSanitizeProjectNameTest.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2023 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class FieldSanitizeProjectNameTest {
+ @Parameterized.Parameters
+ public static Collection<Object[]> testData() {
+ return Arrays.asList(
+ new Object[][] {
+ {"repoName", "repoName"},
+ {"repo_name", "repo_name"},
+ {"repo-name", "repo-name"},
+ {"repo/name", "repo_0x2F_name"},
+ {"repo+name", "repo_0x2B_name"},
+ {"repo_0x2F_name", "repo_0x_0x2F_name"},
+ });
+ }
+
+ private final String input;
+ private final String expected;
+
+ public FieldSanitizeProjectNameTest(String input, String expected) {
+ this.input = input;
+ this.expected = expected;
+ }
+
+ @Test
+ public void shouldSanitizeProjectName() {
+ Field<String> projectNameField = Field.ofProjectName("test_name").build();
+ String result = projectNameField.formatter().apply(input);
+ assertThat(result).isEqualTo(expected);
+ }
+}
diff --git a/javatests/com/google/gerrit/proto/BUILD b/javatests/com/google/gerrit/proto/BUILD
index 6b98b72..216ef94 100644
--- a/javatests/com/google/gerrit/proto/BUILD
+++ b/javatests/com/google/gerrit/proto/BUILD
@@ -5,7 +5,7 @@
srcs = glob(["*.java"]),
deps = [
"//java/com/google/gerrit/proto",
- "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//java/com/google/gerrit/testing:gerrit-junit",
"//lib:protobuf",
"//lib/truth",
"//lib/truth:truth-proto-extension",
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 4821f20..6e37f61 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -77,6 +77,7 @@
"//lib:jgit",
"//lib:jgit-junit",
"//lib:protobuf",
+ "//lib:roaringbitmap",
"//lib:soy",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
diff --git a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java
similarity index 93%
rename from javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
rename to javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java
index eb2133e..d36f62b 100644
--- a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account.externalids;
+package com.google.gerrit.server.account.externalids.storage.notedb;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
@@ -22,7 +22,9 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.gerrit.entities.Account;
-import com.google.gerrit.server.account.externalids.AllExternalIds.Serializer;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.account.externalids.storage.notedb.AllExternalIds.Serializer;
import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto;
import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto.ExternalIdProto;
import com.google.gerrit.server.config.AuthConfig;
@@ -35,14 +37,14 @@
import org.mockito.Mock;
public class AllExternalIdsTest {
- private ExternalIdFactory externalIdFactory;
+ private ExternalIdFactoryNoteDbImpl externalIdFactory;
@Mock AuthConfig authConfig;
@Before
public void setUp() throws Exception {
externalIdFactory =
- new ExternalIdFactory(
+ new ExternalIdFactoryNoteDbImpl(
new ExternalIdKeyFactory(
new ExternalIdKeyFactory.Config() {
@Override
diff --git a/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java
similarity index 96%
rename from javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
rename to javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java
index 84302a4..bf38148 100644
--- a/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account.externalids;
+package com.google.gerrit.server.account.externalids.storage.notedb;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times;
@@ -23,10 +23,12 @@
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.metrics.DisabledMetricMaker;
-import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdReader;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
import com.google.gerrit.server.account.externalids.testing.ExternalIdTestUtil;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AllUsersNameProvider;
@@ -63,12 +65,13 @@
private ExternalIdReader externalIdReader;
private ExternalIdReader externalIdReaderSpy;
- private ExternalIdFactory externalIdFactory;
+ private ExternalIdFactoryNoteDbImpl externalIdFactory;
@Mock private AuthConfig authConfig;
@Before
public void setUp() throws Exception {
- externalIdFactory = new ExternalIdFactory(new ExternalIdKeyFactory(() -> false), authConfig);
+ externalIdFactory =
+ new ExternalIdFactoryNoteDbImpl(new ExternalIdKeyFactory(() -> false), authConfig);
externalIdCache = CacheBuilder.newBuilder().build();
repoManager.createRepository(ALL_USERS).close();
externalIdReader =
@@ -268,6 +271,7 @@
return oldState;
}
+ @CanIgnoreReturnValue
private ObjectId insertExternalId(int key, int accountId) throws Exception {
return performExternalIdUpdate(
u -> {
diff --git a/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java b/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java
index 7bdb23c..8ff6825 100644
--- a/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java
+++ b/javatests/com/google/gerrit/server/extensions/events/GitReferenceUpdatedTest.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.extensions.events;
+import static com.google.common.truth.Truth.assertThat;
+
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated.GitBatchRefUpdateEvent;
import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import java.io.IOException;
@@ -34,6 +37,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@@ -49,6 +54,7 @@
@Mock GitReferenceUpdatedListener refUpdatedListener;
@Mock GitBatchRefUpdateListener batchRefUpdateListener;
@Mock EventUtil util;
+ @Captor ArgumentCaptor<GitBatchRefUpdateEvent> eventCaptor;
@Before
public void setup() {
@@ -87,6 +93,36 @@
}
@Test
+ public void shouldPreserveRefsOrderInTheBatchRefsUpdatedEvent() {
+ BatchRefUpdate update = newBatchRefUpdate();
+ ReceiveCommand cmd1 =
+ new ReceiveCommand(
+ ObjectId.zeroId(),
+ ObjectId.fromString("0000000000000000000000000000000000000001"),
+ "refs/changes/01/1/1");
+ ReceiveCommand cmd2 =
+ new ReceiveCommand(
+ ObjectId.zeroId(),
+ ObjectId.fromString("0000000000000000000000000000000000000001"),
+ "refs/changes/01/1/meta");
+ cmd1.setResult(ReceiveCommand.Result.OK);
+ cmd2.setResult(ReceiveCommand.Result.OK);
+ update.addCommand(cmd1);
+ update.addCommand(cmd2);
+
+ GitReferenceUpdated event =
+ new GitReferenceUpdated(
+ new PluginSetContext<>(batchRefUpdateListeners, PluginMetrics.DISABLED_INSTANCE),
+ new PluginSetContext<>(refUpdatedListeners, PluginMetrics.DISABLED_INSTANCE),
+ util);
+ event.fire(Project.NameKey.parse("project"), update, updater);
+ Mockito.verify(batchRefUpdateListener, Mockito.times(1))
+ .onGitBatchRefUpdate(eventCaptor.capture());
+ GitBatchRefUpdateEvent batchEvent = eventCaptor.getValue();
+ assertThat(batchEvent.getRefNames()).isInOrder();
+ }
+
+ @Test
public void RefUpdateEventAndRefsUpdateEventAreFired_RefUpdate() throws Exception {
String ref = "refs/heads/master";
RefUpdate update = newRefUpdate(ref);
diff --git a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
index 0112f88..05965fb 100644
--- a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
+++ b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
@@ -62,7 +62,8 @@
DeleteZombieCommentsRefs clean =
new DeleteZombieCommentsRefs(
new AllUsersName("All-Users"), repoManager, null, (msg) -> {});
- clean.execute();
+ int deletedDrafts = clean.execute();
+ assertThat(deletedDrafts).isEqualTo(1);
/* Check that ref1 still exists, and ref2 is deleted */
assertThat(usersRepo.exactRef(ref1.getName())).isNotNull();
@@ -83,7 +84,8 @@
DeleteZombieCommentsRefs clean =
new DeleteZombieCommentsRefs(
new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {});
- clean.execute();
+ int deletedDrafts = clean.execute();
+ assertThat(deletedDrafts).isEqualTo(1);
/* ref1 not deleted, ref2 deleted, ref3 not deleted because of the clean percentage */
assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(2);
@@ -92,7 +94,8 @@
assertThat(usersRepo.exactRef(ref3.getName())).isNotNull();
/* Re-execute the cleanup and make sure nothing's changed */
- clean.execute();
+ deletedDrafts = clean.execute();
+ assertThat(deletedDrafts).isEqualTo(0);
assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(2);
assertThat(usersRepo.exactRef(ref1.getName())).isNotNull();
assertThat(usersRepo.exactRef(ref2.getName())).isNull();
@@ -104,7 +107,8 @@
new DeleteZombieCommentsRefs(
new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {});
- clean.execute();
+ deletedDrafts = clean.execute();
+ assertThat(deletedDrafts).isEqualTo(1);
/* Now ref3 is deleted */
assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(1);
@@ -140,7 +144,8 @@
DeleteZombieCommentsRefs clean =
new DeleteZombieCommentsRefs(
new AllUsersName("All-Users"), repoManager, null, (msg) -> {});
- clean.execute();
+ int deletedDrafts = clean.execute();
+ assertThat(deletedDrafts).isEqualTo(5001);
assertThat(
usersRepo.getRefDatabase().getRefs().stream()
diff --git a/javatests/com/google/gerrit/server/git/TagSetTest.java b/javatests/com/google/gerrit/server/git/TagSetTest.java
index 4a3c930..0863a2c 100644
--- a/javatests/com/google/gerrit/server/git/TagSetTest.java
+++ b/javatests/com/google/gerrit/server/git/TagSetTest.java
@@ -31,9 +31,8 @@
import com.google.gerrit.server.git.TagSet.CachedRef;
import com.google.gerrit.server.git.TagSet.Tag;
import com.google.inject.TypeLiteral;
+import com.google.protobuf.ByteString;
import java.lang.reflect.Type;
-import java.util.Arrays;
-import java.util.BitSet;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
@@ -41,6 +40,7 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdOwnerMap;
import org.junit.Test;
+import org.roaringbitmap.RoaringBitmap;
public class TagSetTest {
@Test
@@ -55,10 +55,12 @@
ObjectIdOwnerMap<Tag> tags = new ObjectIdOwnerMap<>();
tags.add(
new Tag(
- ObjectId.fromString("cccccccccccccccccccccccccccccccccccccccc"), newBitSet(1, 3, 5)));
+ ObjectId.fromString("cccccccccccccccccccccccccccccccccccccccc"),
+ RoaringBitmap.bitmapOf(1, 3, 5)));
tags.add(
new Tag(
- ObjectId.fromString("dddddddddddddddddddddddddddddddddddddddd"), newBitSet(2, 4, 6)));
+ ObjectId.fromString("dddddddddddddddddddddddddddddddddddddddd"),
+ RoaringBitmap.bitmapOf(2, 4, 6)));
TagSet tagSet = new TagSet(Project.nameKey("project"), refs, tags);
TagSetProto proto = tagSet.toProto();
@@ -91,7 +93,9 @@
byteString(
0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc,
0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc))
- .setFlags(byteString(0x2a))
+ .setFlags(
+ ByteString.copyFromUtf8(
+ ":0\000\000\001\000\000\000\000\000\002\000\020\000\000\000\001\000\003\000\005\000"))
.build())
.addTag(
TagProto.newBuilder()
@@ -99,7 +103,9 @@
byteString(
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd))
- .setFlags(byteString(0x54))
+ .setFlags(
+ ByteString.copyFromUtf8(
+ ":0\000\000\001\000\000\000\000\000\002\000\020\000\000\000\002\000\004\000\006\000"))
.build())
.build());
@@ -132,7 +138,7 @@
assertThatSerializedClass(Tag.class)
.hasFields(
ImmutableMap.<String, Type>builder()
- .put("refFlags", BitSet.class)
+ .put("refFlags", RoaringBitmap.class)
.put("next", ObjectIdOwnerMap.Entry.class)
.put("w1", int.class)
.put("w2", int.class)
@@ -181,10 +187,4 @@
.map(Tag::name)
.collect(ImmutableSortedSet.toImmutableSortedSet(Comparator.naturalOrder()));
}
-
- private BitSet newBitSet(int... bits) {
- BitSet result = new BitSet();
- Arrays.stream(bits).forEach(result::set);
- return result;
- }
}
diff --git a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
index 6d90309..5d9b15d 100644
--- a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
@@ -25,7 +25,7 @@
import com.google.gerrit.entities.AccountGroupMemberAudit;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.server.account.GroupUuid;
-import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
+import com.google.gerrit.server.account.externalids.storage.notedb.DisabledExternalIdCache;
import com.google.gerrit.server.notedb.NoteDbUtil;
import java.time.Instant;
import java.util.Set;
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index e879170..ea8e0a7 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -46,6 +46,7 @@
null,
null,
null,
+ null,
indexes,
null,
null,
diff --git a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
index b1cd8fb..512a1b1 100644
--- a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
@@ -171,7 +171,7 @@
new Description("Latency metric for testing"),
Field.ofInteger("account", Metadata.Builder::accountId).build(),
Field.ofInteger("change", Metadata.Builder::changeId).build(),
- Field.ofString("project", Metadata.Builder::projectName).build());
+ Field.ofProjectName("project").build());
timer3.start(1000000, 123, "foo/bar").close();
assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).hasSize(4);
@@ -206,14 +206,14 @@
metricMaker.newTimer(
"test1/latency",
new Description("Latency metric for testing"),
- Field.ofString("project", Metadata.Builder::projectName).build());
+ Field.ofProjectName("project").build());
timer1.start(null).close();
Timer2<String, String> timer2 =
metricMaker.newTimer(
"test2/latency",
new Description("Latency metric for testing"),
- Field.ofString("project", Metadata.Builder::projectName).build(),
+ Field.ofProjectName("project").build(),
Field.ofString("branch", Metadata.Builder::branchName).build());
timer2.start(null, null).close();
@@ -221,7 +221,7 @@
metricMaker.newTimer(
"test3/latency",
new Description("Latency metric for testing"),
- Field.ofString("project", Metadata.Builder::projectName).build(),
+ Field.ofProjectName("project").build(),
Field.ofString("branch", Metadata.Builder::branchName).build(),
Field.ofString("revision", Metadata.Builder::revision).build());
timer3.start(null, null, null).close();
@@ -270,7 +270,7 @@
new Description("Latency metric for testing"),
Field.ofInteger("account", Metadata.Builder::accountId).build(),
Field.ofInteger("change", Metadata.Builder::changeId).build(),
- Field.ofString("project", Metadata.Builder::projectName).build());
+ Field.ofProjectName("project").build());
timer3.start(1000000, 123, "value3").close();
assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
@@ -317,7 +317,7 @@
new Description("Latency metric for testing"),
Field.ofInteger("account", Metadata.Builder::accountId).build(),
Field.ofInteger("change", Metadata.Builder::changeId).build(),
- Field.ofString("project", Metadata.Builder::projectName).build());
+ Field.ofProjectName("project").build());
timer3.start(1000000, 123, "foo/bar").close();
assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 20e441b..1df4d45 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -30,6 +30,7 @@
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.ChangeDraftUpdate;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.DefaultRefLogIdentityProvider;
import com.google.gerrit.server.FanOutExecutor;
@@ -41,8 +42,8 @@
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.account.ServiceUserClassifier;
-import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
import com.google.gerrit.server.account.externalids.ExternalIdCache;
+import com.google.gerrit.server.account.externalids.storage.notedb.DisabledExternalIdCache;
import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
import com.google.gerrit.server.approval.testing.TestPatchSetApprovalUuidGenerator;
import com.google.gerrit.server.config.AllUsersName;
@@ -217,6 +218,8 @@
throw new UnsupportedOperationException();
});
bind(PatchSetApprovalUuidGenerator.class).to(TestPatchSetApprovalUuidGenerator.class);
+ bind(ChangeDraftUpdate.ChangeDraftUpdateFactory.class)
+ .to(ChangeDraftNotesUpdate.Factory.class);
}
});
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index c2ef72d..a4846be 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -29,6 +29,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.comparing;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.mockito.Mockito.mock;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
@@ -56,8 +57,10 @@
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.util.AccountTemplateUtil;
import com.google.gerrit.server.util.time.TimeUtil;
@@ -77,13 +80,21 @@
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
import org.junit.Test;
public class ChangeNotesTest extends AbstractChangeNotesTest {
- @Inject private DraftCommentNotes.Factory draftNotesFactory;
-
@Inject private ChangeNoteJson changeNoteJson;
+ @Inject private DraftCommentsReader draftCommentsReader;
+
+ private TopicValidator topicValidator;
+
+ @Before
+ public void setUp() throws Exception {
+ topicValidator = mock(TopicValidator.class);
+ }
+
@Test
public void tagChangeMessage() throws Exception {
String tag = "jenkins";
@@ -1303,7 +1314,7 @@
ChangeUpdate update = newUpdate(c, changeOwner);
// Make sure unrelevent update does not set mergedOn.
- update.setTopic("topic");
+ update.setTopic("topic", topicValidator);
update.commit();
assertThat(newNotes(c).getMergedOn()).isEmpty();
}
@@ -1630,14 +1641,14 @@
// set topic
String topic = "myTopic";
ChangeUpdate update = newUpdate(c, changeOwner);
- update.setTopic(topic);
+ update.setTopic(topic, topicValidator);
update.commit();
notes = newNotes(c);
assertThat(notes.getChange().getTopic()).isEqualTo(topic);
// clear topic by setting empty string
update = newUpdate(c, changeOwner);
- update.setTopic("");
+ update.setTopic("", topicValidator);
update.commit();
notes = newNotes(c);
assertThat(notes.getChange().getTopic()).isNull();
@@ -1645,21 +1656,21 @@
// set other topic
topic = "otherTopic";
update = newUpdate(c, changeOwner);
- update.setTopic(topic);
+ update.setTopic(topic, topicValidator);
update.commit();
notes = newNotes(c);
assertThat(notes.getChange().getTopic()).isEqualTo(topic);
// clear topic by setting null
update = newUpdate(c, changeOwner);
- update.setTopic(null);
+ update.setTopic(null, topicValidator);
update.commit();
notes = newNotes(c);
assertThat(notes.getChange().getTopic()).isNull();
// check invalid topic
ChangeUpdate failingUpdate = newUpdate(c, changeOwner);
- assertThrows(ValidationException.class, () -> failingUpdate.setTopic("\""));
+ assertThrows(ValidationException.class, () -> failingUpdate.setTopic("\"", topicValidator));
}
@Test
@@ -1671,7 +1682,7 @@
// An update doesn't affect the Change-Id
ChangeUpdate update = newUpdate(c, changeOwner);
- update.setTopic("topic"); // Change something to get a new commit.
+ update.setTopic("topic", topicValidator); // Change something to get a new commit.
update.commit();
assertThat(notes.getChange().getKey()).isEqualTo(c.getKey());
@@ -1700,7 +1711,7 @@
// An update doesn't affect the branch
ChangeUpdate update = newUpdate(c, changeOwner);
- update.setTopic("topic"); // Change something to get a new commit.
+ update.setTopic("topic", topicValidator); // Change something to get a new commit.
update.commit();
assertThat(newNotes(c).getChange().getDest()).isEqualTo(expectedBranch);
@@ -1721,7 +1732,7 @@
// An update doesn't affect the owner
ChangeUpdate update = newUpdate(c, otherUser);
- update.setTopic("topic"); // Change something to get a new commit.
+ update.setTopic("topic", topicValidator); // Change something to get a new commit.
update.commit();
assertThat(newNotes(c).getChange().getOwner()).isEqualTo(changeOwner.getAccountId());
}
@@ -1735,7 +1746,7 @@
// An update doesn't affect the createdOn timestamp.
ChangeUpdate update = newUpdate(c, changeOwner);
- update.setTopic("topic"); // Change something to get a new commit.
+ update.setTopic("topic", topicValidator); // Change something to get a new commit.
update.commit();
assertThat(newNotes(c).getChange().getCreatedOn()).isEqualTo(createdOn);
}
@@ -1750,7 +1761,7 @@
// Various kinds of updates that update the timestamp.
ChangeUpdate update = newUpdate(c, changeOwner);
- update.setTopic("topic"); // Change something to get a new commit.
+ update.setTopic("topic", topicValidator); // Change something to get a new commit.
update.commit();
Instant ts2 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts2).isGreaterThan(ts1);
@@ -2856,8 +2867,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId))
- .containsExactlyEntriesIn(ImmutableListMultimap.of(commitId, comment1));
+ assertThat(notes.getDraftComments(otherUserId)).containsExactly(comment1);
assertThat(notes.getHumanComments()).isEmpty();
update = newUpdate(c, otherUser);
@@ -2920,12 +2930,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId))
- .containsExactlyEntriesIn(
- ImmutableListMultimap.of(
- commitId, comment1,
- commitId, comment2))
- .inOrder();
+ assertThat(notes.getDraftComments(otherUserId)).containsExactly(comment1, comment2).inOrder();
assertThat(notes.getHumanComments()).isEmpty();
// Publish first draft.
@@ -2935,8 +2940,7 @@
update.commit();
notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId))
- .containsExactlyEntriesIn(ImmutableListMultimap.of(commitId, comment2));
+ assertThat(notes.getDraftComments(otherUserId)).containsExactly(comment2);
assertThat(notes.getHumanComments())
.containsExactlyEntriesIn(ImmutableListMultimap.of(commitId, comment1));
}
@@ -2991,11 +2995,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId))
- .containsExactlyEntriesIn(
- ImmutableListMultimap.of(
- commitId1, baseComment,
- commitId2, psComment));
+ assertThat(notes.getDraftComments(otherUserId)).containsExactly(baseComment, psComment);
assertThat(notes.getHumanComments()).isEmpty();
// Publish both comments.
@@ -3387,8 +3387,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId).get(commitId1))
- .containsExactly(comment1, comment2);
+ assertThat(notes.getDraftComments(otherUserId)).containsExactly(comment1, comment2);
assertThat(notes.getHumanComments()).isEmpty();
update = newUpdate(c, otherUser);
@@ -3397,7 +3396,7 @@
update.commit();
notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId).get(commitId1)).containsExactly(comment1);
+ assertThat(notes.getDraftComments(otherUserId)).containsExactly(comment1);
assertThat(notes.getHumanComments().get(commitId1)).containsExactly(comment2);
}
@@ -3473,20 +3472,24 @@
// Re-add draft version of comment2 back to draft ref without updating
// change ref. Simulates the case where deleting the draft failed
// non-atomically after adding the published comment succeeded.
- ChangeDraftUpdate draftUpdate = newUpdate(c, otherUser).createDraftUpdateIfNull();
- draftUpdate.putComment(comment2);
- try (NoteDbUpdateManager manager = updateManagerFactory.create(c.getProject())) {
- manager.add(draftUpdate);
- testRefAction(() -> manager.execute());
+ Optional<ChangeDraftNotesUpdate> draftUpdate =
+ ChangeDraftNotesUpdate.asChangeDraftNotesUpdate(
+ newUpdate(c, otherUser).createDraftUpdateIfNull());
+ if (draftUpdate.isPresent()) {
+ draftUpdate.get().putDraftComment(comment2);
+ try (NoteDbUpdateManager manager = updateManagerFactory.create(c.getProject())) {
+ manager.add(draftUpdate.get());
+ testRefAction(() -> manager.execute());
+ }
}
// Looking at drafts directly shows the zombie comment.
- DraftCommentNotes draftNotes = draftNotesFactory.create(c.getId(), otherUserId);
- assertThat(draftNotes.load().getComments().get(commitId1)).containsExactly(comment1, comment2);
+ assertThat(draftCommentsReader.getDraftsByChangeAndDraftAuthor(c.getId(), otherUserId))
+ .containsExactly(comment1, comment2);
// Zombie comment is filtered out of drafts via ChangeNotes.
ChangeNotes notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId).get(commitId1)).containsExactly(comment1);
+ assertThat(notes.getDraftComments(otherUserId)).containsExactly(comment1);
assertThat(notes.getHumanComments().get(commitId1)).containsExactly(comment2);
update = newUpdate(c, otherUser);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index d13ccdd..064cd89 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -23,6 +23,7 @@
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
import static java.util.Objects.requireNonNull;
+import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -40,6 +41,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerStatusUpdate;
+import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.notedb.ChangeNoteUtil.AttentionStatusInNoteDb;
import com.google.gerrit.server.notedb.CommitRewriter.BackfillResult;
import com.google.gerrit.server.notedb.CommitRewriter.CommitDiff;
@@ -74,10 +76,14 @@
private @Inject CommitRewriter rewriter;
@Inject private ChangeNoteUtil changeNoteUtil;
+ private TopicValidator topicValidator;
+
private static final Gson gson = OutputFormat.JSON_COMPACT.newGson();
@Before
- public void setUp() throws Exception {}
+ public void setUp() throws Exception {
+ topicValidator = mock(TopicValidator.class);
+ }
@After
public void cleanUp() throws Exception {
@@ -1500,7 +1506,7 @@
ChangeUpdate invalidMergedMessageUpdate = newUpdate(c, changeOwner);
invalidMergedMessageUpdate.setChangeMessage(
"Change has been successfully merged by " + changeOwner.getName());
- invalidMergedMessageUpdate.setTopic("");
+ invalidMergedMessageUpdate.setTopic("", topicValidator);
commitsToFix.add(invalidMergedMessageUpdate.commit());
ChangeUpdate invalidCherryPickedMessageUpdate = newUpdate(c, changeOwner);
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index f904d26..2c012fa 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -74,6 +74,7 @@
import com.google.gerrit.extensions.api.changes.AttentionSetInput;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
+import com.google.gerrit.extensions.api.changes.CustomKeyedValuesInput;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -96,6 +97,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.PaginationType;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Predicate;
@@ -119,6 +121,7 @@
import com.google.gerrit.server.change.ChangeTriplet;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -181,6 +184,7 @@
@Inject protected AccountManager accountManager;
@Inject protected AllUsersName allUsersName;
@Inject protected BatchUpdate.Factory updateFactory;
+ @Inject protected AllProjectsName allProjectsName;
@Inject protected ChangeInserter.Factory changeFactory;
@Inject protected ChangeQueryBuilder queryBuilder;
@Inject protected GerritApi gApi;
@@ -3849,7 +3853,7 @@
@GerritConfig(name = "accounts.visibility", value = "NONE")
@Test
- public void userDestination() throws Exception {
+ public void namedDestination() throws Exception {
createProject("repo1");
Change change1 = insert("repo1", newChange("repo1"));
createProject("repo2");
@@ -3859,6 +3863,8 @@
.hasMessageThat()
.isEqualTo("Unknown named destination: foo");
+ String group = "test-group";
+ AccountGroup.UUID groupId = groupOperations.newGroup().name(group).create();
Account.Id anotherUserId =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
String destination1 = "refs/heads/master\trepo1";
@@ -3902,6 +3908,13 @@
Ref anotherUserRef = allUsers.getRepository().exactRef(anotherRefsUsers);
assertThat(userRef).isNotNull();
assertThat(anotherUserRef).isNotNull();
+
+ String groupRef = RefNames.refsGroups(groupId);
+ allUsers.branch(groupRef).commit().add("destinations/destination1", destination1).create();
+ allUsers.branch(groupRef).commit().add("destinations/destination2", destination2).create();
+ allUsers.branch(groupRef).commit().add("destinations/destination3", destination3).create();
+ allUsers.branch(groupRef).commit().add("destinations/destination4", destination4).create();
+ assertThat(allUsers.getRepository().exactRef(groupRef)).isNotNull();
}
assertQuery("destination:destination1", change1);
@@ -3927,15 +3940,35 @@
assertThatQueryException("destination:destination3,user=" + userId)
.hasMessageThat()
.isEqualTo(String.format("Account '%s' not found", userId));
+
+ // Group destinations
+ requestContext.setContext(newRequestContext(userId));
+ assertThatQueryException("destination:non-existent-dest,group=" + group)
+ .hasMessageThat()
+ .isEqualTo("Unknown named destination: non-existent-dest");
+ assertThatQueryException("destination:destination1,group=non-existent-group")
+ .hasMessageThat()
+ .isEqualTo("Group non-existent-group not found");
+ assertThatQueryException("destination:destination1,group=" + group + ",user=" + userId)
+ .hasMessageThat()
+ .isEqualTo("User and group arguments are mutually exclusive");
+
+ assertQuery("destination:destination1,group=" + group, change1);
+ assertQuery("destination:name=destination1,group=" + group, change1);
+ assertQuery("destination:group=" + group + ",destination2", change2);
+ assertQuery("destination:group=" + group + ",name=destination3", change2, change1);
+ assertQuery("destination:destination4,group=" + group);
}
@GerritConfig(name = "accounts.visibility", value = "NONE")
@Test
- public void userQuery() throws Exception {
+ public void namedQuery() throws Exception {
repo = createAndOpenProject("repo");
Change change1 = insert("repo", newChange(repo));
Change change2 = insert("repo", newChangeForBranch(repo, "stable"));
+ String group = "test-group";
+ AccountGroup.UUID groupId = groupOperations.newGroup().name(group).create();
Account.Id anotherUserId = createAccount("anotheruser");
String queryListText =
"query1\tproject:repo\n"
@@ -3952,14 +3985,20 @@
new TestRepository<>(repoManager.openRepository(allUsersName));
MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName);
MetaDataUpdate anotherMd = metaDataUpdateFactory.create(allUsersName)) {
- VersionedAccountQueries queries = VersionedAccountQueries.forUser(userId);
+ VersionedAccountQueries queries =
+ VersionedAccountQueries.forBranch(
+ BranchNameKey.create(allUsersName, RefNames.refsUsers(userId)));
queries.load(md);
queries.setQueryList(queryListText);
queries.commit(md);
- VersionedAccountQueries anotherQueries = VersionedAccountQueries.forUser(anotherUserId);
+ VersionedAccountQueries anotherQueries =
+ VersionedAccountQueries.forBranch(
+ BranchNameKey.create(allUsersName, RefNames.refsUsers(anotherUserId)));
anotherQueries.load(anotherMd);
anotherQueries.setQueryList(anotherQueryListText);
anotherQueries.commit(anotherMd);
+
+ allUsers.branch(RefNames.refsGroups(groupId)).commit().add("queries", queryListText).create();
}
assertThat(gApi.accounts().self().get()._accountId).isEqualTo(userId.get());
@@ -3990,6 +4029,23 @@
assertQuery("query:query6,user=" + anotherUserId, change1);
assertQuery("query:user=" + anotherUserId + ",query7", change2);
assertQuery("query:query8,user=" + anotherUserId);
+
+ // Group queries
+ assertThatQueryException("query:non-existent,group=" + group)
+ .hasMessageThat()
+ .isEqualTo("Unknown named query: non-existent");
+ assertThatQueryException("query:query1,group=non-existent-group")
+ .hasMessageThat()
+ .isEqualTo("Group non-existent-group not found");
+ assertThatQueryException("query:query1,group=" + group + ",user=" + userId)
+ .hasMessageThat()
+ .isEqualTo("User and group arguments are mutually exclusive");
+
+ assertQuery("query:name=query1,group=" + group, change1, change2);
+ assertQuery("query:query1,group=" + group, change1, change2);
+ assertQuery("query:group=" + group + ",name=query2", change2);
+ assertQuery("query:group=" + group + ",query4");
+ assertQuery("query:name=query4,group=" + group);
}
@Test
@@ -4135,6 +4191,38 @@
.contains("'is:mergeable' operator is not supported on this gerrit host");
}
+ @Test
+ public void customKeyedValue() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.CUSTOM_KEYED_VALUES_SPEC)).isTrue();
+
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ CustomKeyedValuesInput in = new CustomKeyedValuesInput();
+ in.add = ImmutableMap.of("workspace", "my-ws");
+ gApi.changes().id(change1.getChangeId()).setCustomKeyedValues(in);
+
+ Change change2 = insert("repo", newChange(repo));
+
+ in = new CustomKeyedValuesInput();
+ in.add = ImmutableMap.of("workspace", "123");
+ gApi.changes().id(change2.getChangeId()).setCustomKeyedValues(in);
+
+ // Insert a change without a KV pair
+ insert("repo", newChange(repo));
+
+ assertThat(customKeyedValues("workspace="))
+ .containsExactly(change1.getChangeId(), change2.getChangeId());
+ assertThat(customKeyedValues("workspace=my")).containsExactly(change1.getChangeId());
+ assertThat(customKeyedValues("workspace=123")).containsExactly(change2.getChangeId());
+ assertThat(customKeyedValues("workspace=foo-bar")).isEmpty();
+ }
+
+ protected List<Integer> customKeyedValues(String query) {
+ return queryProvider.get().byCustomKeyedValue(query).stream()
+ .map(cd -> cd.getId().get())
+ .collect(toList());
+ }
+
protected ChangeInserter newChangeForCommit(TestRepository<Repository> repo, RevCommit commit)
throws Exception {
return newChange(repo, commit, null, null, null, null, false, false);
@@ -4563,4 +4651,8 @@
update.setAllowWriteToNewRef(true);
return update;
}
+
+ PaginationType getCurrentPaginationType() {
+ return config.getEnum("index", null, "paginationType", PaginationType.OFFSET);
+ }
}
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesLatestIndexVersionTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesLatestIndexVersionTest.java
index 4dde452..df6ff5a 100644
--- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesLatestIndexVersionTest.java
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesLatestIndexVersionTest.java
@@ -33,4 +33,11 @@
config.setString("index", null, "paginationType", "SEARCH_AFTER");
return config;
}
+
+ @ConfigSuite.Config
+ public static Config nonePaginationType() {
+ Config config = defaultConfig();
+ config.setString("index", null, "paginationType", "NONE");
+ return config;
+ }
}
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesPreviousIndexVersionTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesPreviousIndexVersionTest.java
index 95896dc..805c99a 100644
--- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesPreviousIndexVersionTest.java
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesPreviousIndexVersionTest.java
@@ -43,4 +43,11 @@
config.setString("index", null, "paginationType", "SEARCH_AFTER");
return config;
}
+
+ @ConfigSuite.Config
+ public static Config nonePaginationType() {
+ Config config = againstPreviousIndexVersion();
+ config.setString("index", null, "paginationType", "NONE");
+ return config;
+ }
}
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
index 6b17bb6..4b87108 100644
--- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
@@ -15,16 +15,24 @@
package com.google.gerrit.server.query.change;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.common.data.GlobalCapability.QUERY_LIMIT;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.index.PaginationType;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.testing.AbstractFakeIndex;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.testing.InMemoryModule;
import com.google.inject.Guice;
@@ -76,22 +84,20 @@
AbstractFakeIndex<?, ?, ?> idx =
(AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex();
newQuery("status:new").withLimit(5).get();
- // Since the limit of the query (i.e. 5) is more than the total number of changes (i.e. 4),
- // only 1 index search is expected.
- assertThat(idx.getQueryCount()).isEqualTo(1);
+ assertThatSearchQueryWasNotPaginated(idx.getQueryCount());
}
@Test
@UseClockStep
public void noLimitQueryPaginates() throws Exception {
+ assumeFalse(PaginationType.NONE == getCurrentPaginationType());
+
try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
- // create 4 changes
insert("repo", newChange(testRepo));
insert("repo", newChange(testRepo));
insert("repo", newChange(testRepo));
insert("repo", newChange(testRepo));
}
-
// Set queryLimit to 2
projectOperations
.project(allProjects)
@@ -106,13 +112,127 @@
// the configured query-limit+1), and then we will paginate to get the remaining
// changes with the second index search.
newQuery("status:new").withNoLimit().get();
- assertThat(idx.getQueryCount()).isEqualTo(2);
+ assertThatSearchQueryWasPaginated(idx.getQueryCount(), 2);
+ }
+
+ @Test
+ @UseClockStep
+ public void noLimitQueryDoesNotPaginatesWithNonePaginationType() throws Exception {
+ assumeTrue(PaginationType.NONE == getCurrentPaginationType());
+ AbstractFakeIndex idx = setupRepoWithFourChanges();
+ newQuery("status:new").withNoLimit().get();
+ assertThatSearchQueryWasNotPaginated(idx.getQueryCount());
+ }
+
+ @Test
+ @UseClockStep
+ public void invisibleChangesNotPaginatedWithNonePaginationType() throws Exception {
+ assumeTrue(PaginationType.NONE == getCurrentPaginationType());
+ AbstractFakeIndex idx = setupRepoWithFourChanges();
+ final int LIMIT = 3;
+
+ projectOperations
+ .project(allProjectsName)
+ .forUpdate()
+ .removeAllAccessSections()
+ .add(allow(Permission.READ).ref("refs/*").group(SystemGroupBackend.REGISTERED_USERS))
+ .update();
+
+ // Set queryLimit to 3
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .add(allowCapability(QUERY_LIMIT).group(ANONYMOUS_USERS).range(0, LIMIT))
+ .update();
+
+ requestContext.setContext(anonymousUserProvider::get);
+ List<ChangeInfo> result = newQuery("status:new").withLimit(LIMIT).get();
+ assertThat(result.size()).isEqualTo(0);
+ assertThatSearchQueryWasNotPaginated(idx.getQueryCount());
+ assertThat(idx.getResultsSizes().get(0)).isEqualTo(LIMIT + 1);
+ }
+
+ @Test
+ @UseClockStep
+ public void invisibleChangesPaginatedWithPagination() throws Exception {
+ assumeFalse(PaginationType.NONE == getCurrentPaginationType());
+
+ AbstractFakeIndex idx = setupRepoWithFourChanges();
+ final int LIMIT = 3;
+
+ projectOperations
+ .project(allProjectsName)
+ .forUpdate()
+ .removeAllAccessSections()
+ .add(allow(Permission.READ).ref("refs/*").group(SystemGroupBackend.REGISTERED_USERS))
+ .update();
+
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .add(allowCapability(QUERY_LIMIT).group(ANONYMOUS_USERS).range(0, LIMIT))
+ .update();
+
+ requestContext.setContext(anonymousUserProvider::get);
+ List<ChangeInfo> result = newQuery("status:new").withLimit(LIMIT).get();
+ assertThat(result.size()).isEqualTo(0);
+ assertThatSearchQueryWasPaginated(idx.getQueryCount(), 2);
+ assertThat(idx.getResultsSizes().get(0)).isEqualTo(LIMIT + 1);
+ assertThat(idx.getResultsSizes().get(1)).isEqualTo(0); // Second query size
}
@Test
@UseClockStep
public void internalQueriesPaginate() throws Exception {
- // create 4 changes
+ assumeFalse(PaginationType.NONE == getCurrentPaginationType());
+ final int LIMIT = 2;
+
+ try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ }
+ // Set queryLimit to 2
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(0, LIMIT))
+ .update();
+ AbstractFakeIndex idx = (AbstractFakeIndex) changeIndexCollection.getSearchIndex();
+ // 2 index searches are expected. The first index search will run with size 3 (i.e.
+ // the configured query-limit+1), and then we will paginate to get the remaining
+ // changes with the second index search.
+ executeQuery("status:new");
+ assertThat(idx.getQueryCount()).isEqualTo(LIMIT);
+ }
+
+ @Test
+ @UseClockStep
+ @SuppressWarnings("unchecked")
+ public void internalQueriesDoNotPaginateWithNonePaginationType() throws Exception {
+ assumeTrue(PaginationType.NONE == getCurrentPaginationType());
+
+ AbstractFakeIndex idx = setupRepoWithFourChanges();
+ // 1 index search is expected since we are not paginating.
+ executeQuery("status:new");
+ assertThatSearchQueryWasNotPaginated(idx.getQueryCount());
+ }
+
+ @SuppressWarnings("unused")
+ private void executeQuery(String query) throws QueryParseException {
+ List<ChangeData> unused = queryProvider.get().query(queryBuilder.parse(query));
+ }
+
+ private void assertThatSearchQueryWasNotPaginated(int queryCount) {
+ assertThat(queryCount).isEqualTo(1);
+ }
+
+ private void assertThatSearchQueryWasPaginated(int queryCount, int expectedPages) {
+ assertThat(queryCount).isEqualTo(expectedPages);
+ }
+
+ private AbstractFakeIndex setupRepoWithFourChanges() throws Exception {
try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
insert("repo", newChange(testRepo));
insert("repo", newChange(testRepo));
@@ -127,14 +247,6 @@
.add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(0, 2))
.update();
- AbstractFakeIndex<?, ?, ?> idx =
- (AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex();
-
- // 2 index searches are expected. The first index search will run with size 3 (i.e.
- // the configured query-limit+1), and then we will paginate to get the remaining
- // changes with the second index search.
- List<ChangeData> matches = queryProvider.get().query(queryBuilder.parse("status:new"));
- assertThat(matches).hasSize(4);
- assertThat(idx.getQueryCount()).isEqualTo(2);
+ return (AbstractFakeIndex) changeIndexCollection.getSearchIndex();
}
}
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesLatestIndexVersionTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesLatestIndexVersionTest.java
index 4587943..bfd1bd6 100644
--- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesLatestIndexVersionTest.java
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesLatestIndexVersionTest.java
@@ -30,4 +30,11 @@
config.setString("index", null, "paginationType", "SEARCH_AFTER");
return config;
}
+
+ @ConfigSuite.Config
+ public static Config nonePaginationType() {
+ Config config = defaultConfig();
+ config.setString("index", null, "paginationType", "NONE");
+ return config;
+ }
}
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesPreviousIndexVersionTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesPreviousIndexVersionTest.java
index 1782697..f93a2a7 100644
--- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesPreviousIndexVersionTest.java
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesPreviousIndexVersionTest.java
@@ -40,4 +40,11 @@
config.setString("index", null, "paginationType", "SEARCH_AFTER");
return config;
}
+
+ @ConfigSuite.Config
+ public static Config nonePaginationType() {
+ Config config = againstPreviousIndexVersion();
+ config.setString("index", null, "paginationType", "NONE");
+ return config;
+ }
}
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractFakeQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractFakeQueryGroupsTest.java
new file mode 100644
index 0000000..bf224f0
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/group/AbstractFakeQueryGroupsTest.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2023 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.query.group;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.index.PaginationType;
+import com.google.gerrit.index.testing.AbstractFakeIndex;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public abstract class AbstractFakeQueryGroupsTest extends AbstractQueryGroupsTest {
+
+ @Inject private GroupIndexCollection groupIndexCollection;
+
+ @Override
+ protected Injector createInjector() {
+ Config fakeConfig = new Config(config);
+ InMemoryModule.setDefaults(fakeConfig);
+ fakeConfig.setString("index", null, "type", "fake");
+ return Guice.createInjector(new InMemoryModule(fakeConfig));
+ }
+
+ @Before
+ public void resetQueryCount() {
+ ((AbstractFakeIndex<?, ?, ?>) groupIndexCollection.getSearchIndex()).resetQueryCount();
+ }
+
+ @Test
+ public void internalQueriesDoNotPaginateWithNonePaginationType() throws Exception {
+ assumeTrue(PaginationType.NONE == getCurrentPaginationType());
+
+ final int GROUPS_CREATED_SIZE = 2;
+ List<GroupInfo> groupsCreated = new ArrayList<>();
+ for (int i = 0; i < GROUPS_CREATED_SIZE; i++) {
+ groupsCreated.add(createGroupThatIsVisibleToAll(name("group-" + i)));
+ }
+
+ List<GroupInfo> result = assertQuery(newQuery("is:visibletoall"), groupsCreated);
+ assertThat(result.size()).isEqualTo(GROUPS_CREATED_SIZE);
+ assertThat(result.get(result.size() - 1)._moreGroups).isNull();
+ assertThatSearchQueryWasNotPaginated();
+ }
+
+ PaginationType getCurrentPaginationType() {
+ return config.getEnum("index", null, "paginationType", PaginationType.OFFSET);
+ }
+
+ private void assertThatSearchQueryWasNotPaginated() {
+ assertThat(getQueryCount()).isEqualTo(1);
+ }
+
+ private int getQueryCount() {
+ AbstractFakeIndex<?, ?, ?> idx =
+ (AbstractFakeIndex<?, ?, ?>) groupIndexCollection.getSearchIndex();
+ return idx.getQueryCount();
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
index e877c81..550cb41 100644
--- a/javatests/com/google/gerrit/server/query/group/BUILD
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -1,7 +1,10 @@
load("@rules_java//java:defs.bzl", "java_library")
load("//tools/bzl:junit.bzl", "junit_tests")
-ABSTRACT_QUERY_TEST = ["AbstractQueryGroupsTest.java"]
+ABSTRACT_QUERY_TEST = [
+ "AbstractQueryGroupsTest.java",
+ "AbstractFakeQueryGroupsTest.java",
+]
java_library(
name = "abstract_query_tests",
@@ -14,6 +17,7 @@
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/index",
+ "//java/com/google/gerrit/index/testing",
"//java/com/google/gerrit/lifecycle",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/schema",
diff --git a/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
index e4f228a..d347716 100644
--- a/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
@@ -25,12 +25,26 @@
import java.util.Map;
import org.eclipse.jgit.lib.Config;
-public class FakeQueryGroupsTest extends AbstractQueryGroupsTest {
+public class FakeQueryGroupsTest extends AbstractFakeQueryGroupsTest {
@ConfigSuite.Default
public static Config defaultConfig() {
return IndexConfig.createForFake();
}
+ @ConfigSuite.Config
+ public static Config searchAfterPaginationType() {
+ Config config = defaultConfig();
+ config.setString("index", null, "paginationType", "SEARCH_AFTER");
+ return config;
+ }
+
+ @ConfigSuite.Config
+ public static Config nonePaginationType() {
+ Config config = defaultConfig();
+ config.setString("index", null, "paginationType", "NONE");
+ return config;
+ }
+
@ConfigSuite.Configs
public static Map<String, Config> againstPreviousIndexVersion() {
// the current schema version is already tested by the inherited default config suite
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index b119104..47d485d 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -23,7 +23,8 @@
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
@@ -69,13 +70,18 @@
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
-import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
+/**
+ * Tests queries against the project index.
+ *
+ * <p>Note, returned projects are sorted by name. Projects that start with a capital letter are
+ * returned first.
+ */
@Ignore
public abstract class AbstractQueryProjectsTest extends GerritServerTests {
@Rule public final GerritTestName testName = new GerritTestName();
@@ -112,6 +118,8 @@
protected Injector injector;
protected AccountInfo currentUserInfo;
protected CurrentUser user;
+ protected ProjectInfo allProjectsInfo;
+ protected ProjectInfo allUsersInfo;
protected abstract Injector createInjector();
@@ -141,6 +149,13 @@
user = userFactory.create(userId);
requestContext.setContext(newRequestContext(userId));
currentUserInfo = gApi.accounts().id(userId.get()).get();
+
+ // All-Projects and All-Users are not indexed, index them now.
+ gApi.projects().name(allProjects.get()).index(/* indexChildren= */ false);
+ gApi.projects().name(allUsers.get()).index(/* indexChildren= */ false);
+
+ allProjectsInfo = gApi.projects().name(allProjects.get()).get();
+ allUsersInfo = gApi.projects().name(allUsers.get()).get();
}
protected void initAfterLifecycleStart() throws Exception {}
@@ -163,6 +178,13 @@
}
@Test
+ public void byEmptyQuery() throws Exception {
+ ProjectInfo project1 = createProject(name("project1"));
+ ProjectInfo project2 = createProject(name("project2"));
+ assertQuery("", allProjectsInfo, allUsersInfo, project1, project2);
+ }
+
+ @Test
public void byName() throws Exception {
assertQuery("name:project");
assertQuery("name:non-existing");
@@ -170,6 +192,8 @@
ProjectInfo project = createProject(name("project"));
assertQuery("name:" + project.name, project);
+ assertQuery("name:" + allProjects.get(), allProjectsInfo);
+ assertQuery("name:" + allUsers.get(), allUsersInfo);
// only exact match
ProjectInfo projectWithHyphen = createProject(name("project-with-hyphen"));
@@ -178,6 +202,49 @@
}
@Test
+ public void byPrefix() throws Exception {
+ assume().that(getSchemaVersion() >= 8).isTrue();
+
+ assertQuery("prefix:project");
+ assertQuery("prefix:non-existing");
+ assertQuery("prefix:All", allProjectsInfo, allUsersInfo);
+ assertQuery("prefix:All-", allProjectsInfo, allUsersInfo);
+
+ ProjectInfo project1 = createProject(name("project-1"));
+ ProjectInfo project2 = createProject(name("project-2"));
+ ProjectInfo testProject = createProject(name("test-project"));
+
+ assertQuery("prefix:project", project1, project2);
+ assertQuery("prefix:test", testProject);
+ assertQuery("prefix:TEST");
+ }
+
+ @Test
+ public void byPrefixWithOtherCase() throws Exception {
+ assume().that(getSchemaVersion() >= 8).isTrue();
+
+ assertQuery("prefix:all");
+
+ createProject(name("test-project"));
+ assertQuery("prefix:TEST");
+ }
+
+ @Test
+ public void bySubstring() throws Exception {
+ assertQuery("substring:non-existing");
+
+ ProjectInfo project1 = createProject(name("project-1"));
+ ProjectInfo project2 = createProject(name("project-2"));
+ ProjectInfo testProject = createProject(name("test-project"));
+ ProjectInfo myTests = createProject(name("MY-TESTS"));
+
+ assertQuery("substring:project", allProjectsInfo, project1, project2, testProject);
+ assertQuery("substring:PROJECT", allProjectsInfo, project1, project2, testProject);
+ assertQuery("substring:test", myTests, testProject);
+ assertQuery("substring:TEST", myTests, testProject);
+ }
+
+ @Test
public void byParent() throws Exception {
assertQuery("parent:project");
ProjectInfo parent = createProject(name("parent"));
@@ -188,12 +255,32 @@
@Test
public void byParentOfAllProjects() throws Exception {
- Set<String> excludedProjects = ImmutableSet.of(allProjects.get(), allUsers.get());
- ProjectInfo[] projects =
- gApi.projects().list().get().stream()
- .filter(p -> !excludedProjects.contains(p.name))
- .toArray(s -> new ProjectInfo[s]);
- assertQuery("parent:" + allProjects.get(), projects);
+ assume().that(getSchemaVersion() < 7).isTrue();
+
+ ProjectInfo parent1 = createProject(name("parent1"));
+ createProject(name("child"), parent1.name);
+
+ ProjectInfo parent2 = createProject(name("parent2"));
+ createProject(name("child2"), parent2.name);
+
+ // All-Users should be returned as well, since it's a direct child project under
+ // All-Projects, but it's missing in the result since the parent1 field in the index is not set
+ // for projects that don't have 'access.inheritsFrom' set in project.config (which is the case
+ // for the All-Users project).
+ assertQuery("parent:" + allProjects.get(), parent1, parent2);
+ }
+
+ @Test
+ public void byParentOfAllProjects2() throws Exception {
+ assume().that(getSchemaVersion() >= 7).isTrue();
+
+ ProjectInfo parent1 = createProject(name("parent1"));
+ createProject(name("child"), parent1.name);
+
+ ProjectInfo parent2 = createProject(name("parent2"));
+ createProject(name("child2"), parent2.name);
+
+ assertQuery("parent:" + allProjects.get(), allUsersInfo, parent1, parent2);
}
@Test
@@ -231,7 +318,7 @@
ProjectInfo project1 = createProjectWithState(name("project1"), ProjectState.ACTIVE);
ProjectInfo project2 = createProjectWithState(name("project2"), ProjectState.READ_ONLY);
- assertQuery("state:active", project1);
+ assertQuery("state:active", allProjectsInfo, allUsersInfo, project1);
assertQuery("state:read-only", project2);
}
@@ -274,8 +361,10 @@
String query =
"name:" + project1.name + " OR name:" + project2.name + " OR name:" + project3.name;
List<ProjectInfo> result = assertQuery(query, project1, project2, project3);
+ assertThat(Iterables.getLast(result)._moreProjects).isNull();
- assertQuery(newQuery(query).withLimit(2), result.subList(0, 2));
+ result = assertQuery(newQuery(query).withLimit(2), result.subList(0, 2));
+ assertThat(Iterables.getLast(result)._moreProjects).isTrue();
}
@Test
@@ -343,6 +432,7 @@
return gApi.projects().create(in).get();
}
+ @CanIgnoreReturnValue
protected ProjectInfo createProject(String name, String parent) throws Exception {
ProjectInput in = new ProjectInput();
in.name = name;
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index 53f9d9d..2ae73b3 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -21,6 +21,7 @@
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:guava",
"//lib:jgit",
+ "//lib/errorprone:annotations",
"//lib/guice",
"//lib/truth",
],
diff --git a/lib/BUILD b/lib/BUILD
index 96a5325..5bb3593 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -510,6 +510,16 @@
exports = ["@icu4j//jar"],
)
+java_library(
+ name = "roaringbitmap",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = ["//visibility:public"],
+ exports = [
+ "@roaringbitmap-shims//jar",
+ "@roaringbitmap//jar",
+ ],
+)
+
sh_test(
name = "nongoogle_test",
srcs = ["nongoogle_test.sh"],
diff --git a/modules/jgit b/modules/jgit
index 74fa245..24b6a35 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 74fa245b3c3ccf13afcbec7911c7c8459e48527d
+Subproject commit 24b6a35d30e76317c043f27c6159e57c39b303d8
diff --git a/plugins/download-commands b/plugins/download-commands
index b90e523..42b608a 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit b90e523f589a0e2902823233010163f453243926
+Subproject commit 42b608a64bdb1350656b2ca09643ed4173cd6e73
diff --git a/plugins/gitiles b/plugins/gitiles
index 20f65c2..4e8bd70 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 20f65c2067b9190d1c85fbf61e5d72edf4493724
+Subproject commit 4e8bd706e87eb11e3cfe2bfa9bbcb29020f39482
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index dbd6820..ba74d49 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit dbd68200d867513e2c0449798476e275aaf08cfd
+Subproject commit ba74d4969462c2592bcf97868dd76c33041d47b2
diff --git a/polygerrit-ui/app/api/annotation.ts b/polygerrit-ui/app/api/annotation.ts
index c394ef7..df9af40 100644
--- a/polygerrit-ui/app/api/annotation.ts
+++ b/polygerrit-ui/app/api/annotation.ts
@@ -3,8 +3,8 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {CoverageRange} from './diff';
-import {ChangeInfo} from './rest-api';
+import {CoverageRange, FileRange, TokenHighlightEventDetails} from './diff';
+import {BasePatchSetNum, ChangeInfo, RevisionPatchSetNum} from './rest-api';
/**
* This is the callback object that Gerrit calls once for each diff. Gerrit
@@ -19,6 +19,20 @@
change?: ChangeInfo
) => Promise<Array<CoverageRange> | undefined>;
+export declare interface DiffDetails {
+ change: ChangeInfo;
+ basePatchNum: BasePatchSetNum;
+ patchNum: RevisionPatchSetNum;
+ fileRange: FileRange;
+ /** @deprecated rely on fileRange.path */
+ path: string;
+}
+
+export declare type TokenHoverListener = (
+ diff: DiffDetails,
+ highlight?: TokenHighlightEventDetails
+) => void;
+
export declare interface AnnotationPluginApi {
/**
* The specified function will be called when a gr-diff component is built,
@@ -29,4 +43,15 @@
* provider of the first call.
*/
setCoverageProvider(coverageProvider: CoverageProvider): void;
+
+ /**
+ * Experimental endpoint for calling a function when a gr-diff token is
+ * hovered.
+ *
+ * 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;
}
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 9963fdc..24110ed 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -10,7 +10,7 @@
*/
import {CursorMoveResult} from './core';
-import {CommentRange} from './rest-api';
+import {BasePatchSetNum, CommentRange, RevisionPatchSetNum} from './rest-api';
/**
* Diff type in preferences
@@ -306,6 +306,16 @@
code_range: LineRange;
}
+export interface FileRange {
+ basePath?: string;
+ path: string;
+}
+
+export interface PatchRange {
+ patchNum: RevisionPatchSetNum;
+ basePatchNum: BasePatchSetNum;
+}
+
/**
* LOST LineNumber is for ported comments without a range, they have their own
* line number and are added on top of the FILE row in <gr-diff>.
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index d3d012d..4aa3aaa 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -32,6 +32,7 @@
REVERT_SUBMISSION = 'revert_submission',
POST_REVERT = 'postrevert',
ADMIN_MENU_LINKS = 'admin-menu-links',
+ SHOW_DIFF = 'showdiff',
}
export declare interface PluginApi {
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index 5b6019e..9869802 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -376,7 +376,6 @@
submitted?: Timestamp;
submitter?: AccountInfo;
starred?: boolean; // not set if false
- stars?: StarLabel[];
submit_type?: SubmitType;
mergeable?: boolean;
submittable?: boolean;
@@ -517,6 +516,31 @@
}
/**
+ * The CommentInfo entity contains information about an inline comment.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info
+ */
+export interface CommentInfo {
+ id: UrlEncodedCommentId;
+ updated: Timestamp;
+ // TODO(TS): Make this required. Every comment must have patch_set set.
+ patch_set?: RevisionPatchSetNum;
+ path?: string;
+ side?: CommentSide;
+ parent?: number;
+ line?: number;
+ range?: CommentRange;
+ in_reply_to?: UrlEncodedCommentId;
+ message?: string;
+ author?: AccountInfo;
+ tag?: string;
+ unresolved?: boolean;
+ change_message_id?: string;
+ commit_id?: string;
+ context_lines?: ContextLine[];
+ source_content_type?: string;
+}
+
+/**
* The CommentRange entity describes the range of an inline comment.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-range
*
@@ -543,6 +567,15 @@
end_character: number;
}
+/**
+ * The side on which the comment was added
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info
+ */
+export enum CommentSide {
+ REVISION = 'REVISION',
+ PARENT = 'PARENT',
+}
+
export declare interface ConfigListParameterInfo
extends ConfigParameterInfoBase {
type: ConfigParameterInfoType.LIST;
@@ -572,6 +605,15 @@
inherited_value?: string;
}
+/**
+ * The ContextLine entity contains the line number and line text of a single line of the source file content.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#context-line
+ */
+export interface ContextLine {
+ line_number: number;
+ context_line: string;
+}
+
// https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#contributor-agreement-info
export declare interface ContributorAgreementInfo {
name: string;
@@ -1033,7 +1075,6 @@
*/
export type SshdInfo = {};
-export type StarLabel = BrandType<string, '_startLabel'>;
// Timestamps are given in UTC and have the format
// "'yyyy-mm-dd hh:mm:ss.fffffffff'"
// where "'ffffffffff'" represents nanoseconds.
@@ -1231,3 +1272,6 @@
): res is Base64FileContent {
return (res as Base64FileContent).ok;
}
+
+// The URL encoded UUID of the comment
+export type UrlEncodedCommentId = BrandType<string, '_urlEncodedCommentId'>;
diff --git a/polygerrit-ui/app/api/suggestions.ts b/polygerrit-ui/app/api/suggestions.ts
new file mode 100644
index 0000000..f75f6e0
--- /dev/null
+++ b/polygerrit-ui/app/api/suggestions.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {CommentRange, NumericChangeId, RevisionPatchSetNum} from './rest-api';
+
+export declare interface SuggestionsPluginApi {
+ /**
+ * Must only be called once. You cannot register twice. You cannot unregister.
+ */
+ register(provider: SuggestionsProvider): void;
+}
+
+export declare interface SuggestCodeRequest {
+ prompt: string;
+ changeNumber: NumericChangeId;
+ patchsetNumber: RevisionPatchSetNum;
+ filePath: string;
+ range?: CommentRange;
+ lineNumber?: Number;
+}
+
+export declare interface SuggestionsProvider {
+ /**
+ * Gerrit calls this method when ...
+ * - ... user types a comment draft
+ */
+ suggestCode(commentData: SuggestCodeRequest): Promise<SuggestCodeResponse>;
+}
+
+declare interface SuggestCodeResponse {
+ responseCode: ResponseCode;
+ suggestions: Suggestion[];
+}
+
+export declare interface Suggestion {
+ replacement: string;
+ newRange?: CommentRange;
+}
+
+export enum ResponseCode {
+ OK = 'OK',
+ ERROR = 'ERROR',
+}
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index b9ed56b..25d25b0 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -13,6 +13,7 @@
import {
AuthType,
ChangeStatus,
+ CommentSide,
ConfigParameterInfoType,
DefaultDisplayNameConfig,
EditableAccountField,
@@ -32,6 +33,7 @@
export {
AuthType,
ChangeStatus,
+ CommentSide,
ConfigParameterInfoType,
DefaultDisplayNameConfig,
EditableAccountField,
@@ -160,15 +162,6 @@
}
/**
- * The side on which the comment was added
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info
- */
-export enum CommentSide {
- REVISION = 'REVISION',
- PARENT = 'PARENT',
-}
-
-/**
* Allowed app themes
* https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#preferences-input
*/
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
index cb17de7..852f907 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
@@ -13,7 +13,11 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
-import {AdminViewState} from '../../../models/views/admin';
+import {
+ AdminChildView,
+ AdminViewState,
+ createAdminUrl,
+} from '../../../models/views/admin';
// Exported for tests
export interface PluginInfoWithName extends PluginInfo {
@@ -22,8 +26,6 @@
@customElement('gr-plugin-list')
export class GrPluginList extends LitElement {
- readonly path = '/admin/plugins';
-
/**
* URL params passed from the router.
*/
@@ -70,7 +72,7 @@
.items=${this.plugins}
.loading=${this.loading}
.offset=${this.offset}
- .path=${this.path}
+ .path=${createAdminUrl({adminView: AdminChildView.PLUGINS})}
>
<table id="list" class="genericList">
<tbody>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
index 8a5bf47..c65371b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
@@ -37,6 +37,7 @@
import {ProgressStatus} from '../../../constants/constants';
import {StandardLabels} from '../../../utils/label-util';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {ReviewResult} from '../../../types/common';
const change1: ChangeInfo = {
...createChange(),
@@ -208,7 +209,7 @@
);
stubRestApi('saveChangeReview').callsFake(
(_changeNum, _patchNum, _review, errFn) =>
- Promise.resolve(new Response()).then(res => {
+ Promise.resolve(undefined).then(res => {
errFn && errFn();
return res;
})
@@ -359,7 +360,7 @@
);
await selectChange(change);
await element.updateComplete;
- const saveChangeReview = mockPromise<Response>();
+ const saveChangeReview = mockPromise<ReviewResult>();
stubRestApi('saveChangeReview').returns(saveChangeReview);
queryAndAssert<GrButton>(element, '#voteFlowButton').click();
@@ -411,7 +412,7 @@
ProgressStatus.RUNNING
);
- saveChangeReview.resolve({...new Response(), status: 200});
+ saveChangeReview.resolve({});
await waitUntil(
() =>
element.progressByChange.get(1 as NumericChangeId) ===
@@ -445,7 +446,7 @@
stubRestApi('saveChangeReview').callsFake(
(_changeNum, _patchNum, _review, errFn) =>
- Promise.resolve(new Response()).then(res => {
+ Promise.resolve({}).then(res => {
errFn && errFn();
return res;
})
@@ -503,7 +504,7 @@
stubRestApi('saveChangeReview').callsFake(
(_changeNum, _patchNum, _review, errFn) =>
- Promise.resolve(new Response()).then(res => {
+ Promise.resolve(undefined).then(res => {
errFn && errFn();
return res;
})
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
index d9be9ca..e4c413f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icon/gr-icon';
import {fire} from '../../../utils/event-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
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 a99a9e5..57bb875 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
@@ -25,7 +25,6 @@
import {
changeIsOpen,
isOwner,
- ListChangesOption,
listChangesOptionsToHex,
} from '../../../utils/change-util';
import {
@@ -48,6 +47,7 @@
isDetailedLabelInfo,
isQuickLabelInfo,
LabelInfo,
+ ListChangesOption,
NumericChangeId,
PatchSetNumber,
RequestPayload,
@@ -1989,18 +1989,27 @@
}
// private but used in test
- handleCherrypickTap() {
+ async handleCherrypickTap() {
if (!this.change) {
throw new Error('The change property must be set');
}
assertIsDefined(this.confirmCherrypick, 'confirmCherrypick');
this.confirmCherrypick.branch = '' as BranchName;
+ const changes = await this.getCherryPickChanges();
+ if (!changes.length) return;
+ this.confirmCherrypick.updateChanges(changes);
+ this.showActionDialog(this.confirmCherrypick);
+ }
+
+ private async getCherryPickChanges() {
+ if (!this.change) return [];
+ if (!this.change.topic) return [this.change];
const query = `topic: "${this.change.topic}"`;
const options = listChangesOptionsToHex(
ListChangesOption.MESSAGES,
ListChangesOption.ALL_REVISIONS
);
- this.restApiService
+ return this.restApiService
.getChanges(0, query, undefined, options)
.then(changes => {
if (!changes) {
@@ -2008,10 +2017,9 @@
'Change Actions',
new Error('getChanges returns undefined')
);
- return;
+ return [];
}
- this.confirmCherrypick!.updateChanges(changes);
- this.showActionDialog(this.confirmCherrypick!);
+ return changes;
});
}
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 b628cc1..fd4bf73 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
@@ -711,7 +711,7 @@
)
.returns(review);
const saveStub = stubRestApi('saveChangeReview').returns(
- Promise.resolve(new Response())
+ Promise.resolve({})
);
const setReviewOnRevert = element.setReviewOnRevert(changeId) as Promise<
undefined | Response
@@ -1089,8 +1089,9 @@
},
];
setup(async () => {
+ element.change!.topic = 'T' as TopicName;
stubRestApi('getChanges').returns(Promise.resolve(changes));
- element.handleCherrypickTap();
+ await element.handleCherrypickTap();
await element.updateComplete;
const confirmCherrypick = queryAndAssert<GrConfirmCherrypickDialog>(
element,
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 f6a40a2..f6099fa 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
@@ -12,12 +12,8 @@
import {subscribe} from '../../lit/subscription-controller';
import {sharedStyles} from '../../../styles/shared-styles';
import {getAppContext} from '../../../services/app-context';
-import {
- CheckResult,
- CheckRun,
- ErrorMessages,
-} from '../../../models/checks/checks-model';
-import {Action, Category, RunStatus} from '../../../api/checks';
+import {CheckRun, ErrorMessages} from '../../../models/checks/checks-model';
+import {Action, Category, CheckResult, RunStatus} from '../../../api/checks';
import {fireShowTab} from '../../../utils/event-util';
import {
compareByWorstCategory,
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 eaa9215..2276aa4 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
@@ -1436,7 +1436,6 @@
id="fileListHeader"
.account=${this.account}
.change=${this.change}
- .changeNum=${this.changeNum}
.commitInfo=${this.revision?.commit}
.changeUrl=${this.computeChangeUrl()}
.editMode=${this.editMode}
@@ -1875,7 +1874,7 @@
handleReplySent() {
assertIsDefined(this.replyModal);
this.replyModal.close();
- this.getChangeModel().navigateToChangeResetReload();
+ this.getCommentsModel().reloadAllComments();
}
private handleReplyCancel() {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index 080eb0c..c37f8d4 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -40,6 +40,7 @@
import {createSearchUrl} from '../../../models/views/search';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {uuid} from '../../../utils/common-util';
+import {ParsedChangeInfo} from '../../../types/types';
const SUGGESTIONS_LIMIT = 15;
const CHANGE_SUBJECT_LIMIT = 50;
@@ -100,7 +101,7 @@
project?: RepoName;
@property({type: Array})
- changes: ChangeInfo[] = [];
+ changes: (ParsedChangeInfo | ChangeInfo)[] = [];
@state()
private query: AutocompleteQuery;
@@ -395,7 +396,7 @@
`;
}
- containsDuplicateProject(changes: ChangeInfo[]) {
+ containsDuplicateProject(changes: (ChangeInfo | ParsedChangeInfo)[]) {
const projects: {[projectName: string]: boolean} = {};
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
@@ -407,7 +408,7 @@
return false;
}
- updateChanges(changes: ChangeInfo[]) {
+ updateChanges(changes: (ParsedChangeInfo | ChangeInfo)[]) {
this.changes = changes;
this.statuses = {};
changes.forEach(change => {
@@ -447,22 +448,31 @@
return '';
}
- updateStatus(change: ChangeInfo, status: Status) {
+ updateStatus(change: ChangeInfo | ParsedChangeInfo, status: Status) {
this.statuses = {...this.statuses, [change.id]: status};
}
- private computeStatus(change: ChangeInfo, statuses: Statuses) {
+ private computeStatus(
+ change: ChangeInfo | ParsedChangeInfo,
+ statuses: Statuses
+ ) {
if (!change || !statuses || !statuses[change.id])
return ProgressStatus.NOT_STARTED;
return statuses[change.id].status;
}
- computeStatusClass(change: ChangeInfo, statuses: Statuses) {
+ computeStatusClass(
+ change: ChangeInfo | ParsedChangeInfo,
+ statuses: Statuses
+ ) {
if (!change || !statuses || !statuses[change.id]) return '';
return statuses[change.id].status === ProgressStatus.FAILED ? 'error' : '';
}
- private computeError(change: ChangeInfo, statuses: Statuses) {
+ private computeError(
+ change: ChangeInfo | ParsedChangeInfo,
+ statuses: Statuses
+ ) {
if (!change || !statuses || !statuses[change.id]) return '';
if (statuses[change.id].status === ProgressStatus.FAILED) {
return statuses[change.id].msg;
@@ -470,7 +480,7 @@
return '';
}
- private getChangeId(change: ChangeInfo) {
+ private getChangeId(change: ChangeInfo | ParsedChangeInfo) {
return change.change_id.substring(0, 10);
}
@@ -534,13 +544,13 @@
this.message = newMessage;
}
- private generateRandomCherryPickTopic(change: ChangeInfo) {
+ private generateRandomCherryPickTopic(change: ChangeInfo | ParsedChangeInfo) {
const message = `cherrypick-${change.topic}-${uuid()}`;
return message;
}
private handleCherryPickFailed(
- change: ChangeInfo,
+ change: ParsedChangeInfo | ChangeInfo,
response?: Response | null
) {
if (!response) return;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 7bbfd93..d548454 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -320,7 +320,6 @@
!this.allowConflicts ? ' the uploader:' : ''
} <gr-account-chip
.account=${this.allowConflicts ? this.account : this.uploader}
- .hideHovercard=${true}
></gr-account-chip
><span></div>`
)}
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 abb637c..d053928 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
@@ -15,6 +15,7 @@
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '../../shared/gr-file-status/gr-file-status';
+import '../gr-comments-summary/gr-comments-summary';
import {assertIsDefined} from '../../../utils/common-util';
import {asyncForeach} from '../../../utils/async-util';
import {FilesExpandedState} from '../gr-file-list-constants';
@@ -247,18 +248,12 @@
@state()
diffPrefs?: DiffPreferencesInfo;
- @state() numFilesShown = DEFAULT_NUM_FILES_SHOWN;
+ @state() numFilesShown = 0;
@state()
fileListIncrement: number = DEFAULT_NUM_FILES_SHOWN;
// Private but used in tests.
- shownFiles: NormalizedFileInfo[] = [];
-
- @state()
- private reportinShownFilesIncrement = 0;
-
- // Private but used in tests.
@state()
expandedFiles: PatchSetFile[] = [];
@@ -839,12 +834,7 @@
}
if (changedProperties.has('files')) {
this.filesChanged();
- }
- if (
- changedProperties.has('files') ||
- changedProperties.has('numFilesShown')
- ) {
- this.shownFiles = this.computeFilesShown();
+ this.numFilesShown = Math.min(this.files.length, DEFAULT_NUM_FILES_SHOWN);
}
if (changedProperties.has('expandedFiles')) {
this.expandedFilesChanged(changedProperties.get('expandedFiles'));
@@ -1012,7 +1002,10 @@
class="extra-col"
.name=${headerEndpoint}
role="columnheader"
- ></gr-endpoint-decorator>
+ >
+ <gr-endpoint-param name="change" .value=${this.change}>
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
`
);
}
@@ -1024,7 +1017,7 @@
const sizeBarLayout = this.computeSizeBarLayout();
return incrementalRepeat({
- values: this.shownFiles,
+ values: this.files,
mapFn: (f, i) =>
this.renderFileRow(
f as NormalizedFileInfo,
@@ -1035,6 +1028,8 @@
),
initialCount: this.fileListIncrement,
targetFrameRate: 1,
+ startAt: 0,
+ endAt: this.numFilesShown,
});
}
@@ -1045,8 +1040,7 @@
showDynamicColumns: boolean,
showPrependedDynamicColumns: boolean
) {
- this.reportRenderedRow(index);
- const previousFileName = this.shownFiles[index - 1]?.__path;
+ const previousFileName = this.files[index - 1]?.__path;
const patchSetFile = this.computePatchSetFile(file);
return html` <div class="stickyArea">
<div
@@ -1800,10 +1794,10 @@
// expanded list.
const newFiles: PatchSetFile[] = [];
let path: string;
- for (let i = 0; i < this.shownFiles.length; i++) {
- path = this.shownFiles[i].__path;
+ for (let i = 0; i < this.numFilesShown; i++) {
+ path = this.files[i].__path;
if (!this.expandedFiles.some(f => f.path === path)) {
- newFiles.push(this.computePatchSetFile(this.shownFiles[i]));
+ newFiles.push(this.computePatchSetFile(this.files[i]));
}
}
@@ -2226,26 +2220,6 @@
);
}
- private computeFilesShown(): NormalizedFileInfo[] {
- const previousNumFilesShown = this.shownFiles ? this.shownFiles.length : 0;
-
- const filesShown = this.files.slice(0, this.numFilesShown);
- fire(this, 'files-shown-changed', {length: filesShown.length});
-
- // Start the timer for the rendering work here because this is where the
- // shownFiles property is being set, and shownFiles is used in the
- // dom-repeat binding.
- this.reporting.time(Timing.FILE_RENDER);
-
- // How many more files are being shown (if it's an increase).
- this.reportinShownFilesIncrement = Math.max(
- 0,
- filesShown.length - previousNumFilesShown
- );
-
- return filesShown;
- }
-
// Private but used in tests.
updateDiffCursor() {
// Overwrite the cursor's list of diffs:
@@ -2265,6 +2239,9 @@
private incrementNumFilesShown() {
this.numFilesShown += this.fileListIncrement;
+ if (this.numFilesShown > this.files.length) {
+ this.numFilesShown = this.files.length;
+ }
}
private computeFileListControlClass() {
@@ -2472,7 +2449,8 @@
*/
computeSizeBarLayout() {
const stats: SizeBarLayout = createDefaultSizeBarLayout();
- this.shownFiles
+ this.files
+ .slice(0, this.numFilesShown)
.filter(f => !isMagicPath(f.__path))
.forEach(f => {
if (f.lines_inserted) {
@@ -2598,24 +2576,6 @@
return this.filesExpanded === FilesExpandedState.NONE;
}
- /**
- * Method to call via binding when each file list row is rendered. This
- * allows approximate detection of when the dom-repeat has completed
- * rendering.
- *
- * @param index The index of the row being rendered.
- * Private but used in tests.
- */
- reportRenderedRow(index: number) {
- if (index === this.shownFiles.length - 1) {
- setTimeout(() => {
- this.reporting.timeEnd(Timing.FILE_RENDER, {
- count: this.reportinShownFilesIncrement,
- });
- }, 1);
- }
- }
-
private handleReloadingDiffPreference() {
this.getUserModel().getDiffPreferences();
}
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 daf0891..ab9d038 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
@@ -398,6 +398,7 @@
element.files = createFiles(250);
await element.updateComplete;
await waitEventLoop();
+ assert.equal(200, element.numFilesShown);
assert.equal(
queryAll<HTMLDivElement>(element, '.file-row').length,
@@ -422,19 +423,9 @@
await waitEventLoop();
assert.equal(element.numFilesShown, 250);
- assert.equal(element.shownFiles.length, 250);
assert.isTrue(controlRow.classList.contains('invisible'));
});
- test('rendering each row calls the reportRenderedRow method', async () => {
- const renderedStub = sinon.stub(element, 'reportRenderedRow');
- element.files = createFiles(10);
- await element.updateComplete;
-
- assert.equal(queryAll<HTMLDivElement>(element, '.file-row').length, 10);
- assert.equal(renderedStub.callCount, 10);
- });
-
test('calculate totals for patch number', async () => {
element.files = [
{
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
index 13662982..bfece63 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
@@ -5,6 +5,7 @@
*/
import '@polymer/iron-selector/iron-selector';
import '../../shared/gr-button/gr-button';
+import '../../shared/gr-tooltip-content/gr-tooltip-content';
import {sharedStyles} from '../../../styles/shared-styles';
import {css, html, LitElement} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 4abfeff..65d36ec 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -740,9 +740,8 @@
.deleteChangeCommitMessage(this.changeNum, this.message.id)
.then(() => {
this.isDeletingChangeMsg = false;
- // TODO: Fix the type casting. Might actually be a bug.
fire(this, 'change-message-deleted', {
- message: this.message as ChangeMessage,
+ message: this.message!,
});
});
}
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-collapse.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-collapse.ts
index 61136d7..3762cef 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-collapse.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-collapse.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {LitElement, css, html, nothing, TemplateResult} from 'lit';
+import '../../shared/gr-button/gr-button';
import '../../shared/gr-icon/gr-icon';
import {customElement, property} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 9421842..fa44e18 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -58,6 +58,7 @@
Suggestion,
UserId,
isDraft,
+ ChangeViewChangeInfo,
} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrLabelScores} from '../gr-label-scores/gr-label-scores';
@@ -130,6 +131,7 @@
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {ironAnnouncerRequestAvailability} from '../../polymer-util';
+import {GrReviewerUpdatesParser} from '../../shared/gr-rest-api-interface/gr-reviewer-updates-parser';
export enum FocusTarget {
ANY = 'any',
@@ -596,6 +598,13 @@
);
subscribe(
this,
+ () => this.getUserModel().account$,
+ account => {
+ this.account = account;
+ }
+ );
+ subscribe(
+ this,
() => this.getConfigModel().serverConfig$,
config => {
this.serverConfig = config;
@@ -658,10 +667,6 @@
this
);
- this.restApiService.getAccount().then(account => {
- if (account) this.account = account;
- });
-
this.addEventListener(
'comment-editing-changed',
(e: CustomEvent<CommentEditingChangedDetail>) => {
@@ -1408,37 +1413,39 @@
reviewInput.remove_from_attention_set
);
- await this.patchsetLevelGrComment?.save();
+ if (this.patchsetLevelGrComment) {
+ this.patchsetLevelGrComment.disableAutoSaving = true;
+ await this.restApiService.awaitPendingDiffDrafts();
+ const comment = this.patchsetLevelGrComment.convertToCommentInput();
+ if (comment && comment.path && comment.message) {
+ reviewInput.comments ??= {};
+ reviewInput.comments[comment.path] ??= [];
+ reviewInput.comments[comment.path].push(comment);
+ }
+ }
assertIsDefined(this.change, 'change');
reviewInput.reviewers = this.computeReviewers();
const errFn = (r?: Response | null) => this.handle400Error(r);
return this.saveReview(reviewInput, errFn)
- .then(response => {
- if (!response) {
- // Null or undefined response indicates that an error handler
- // took responsibility, so just return.
- return;
- }
- if (!response.ok) {
- fireServerError(response);
- return;
- }
+ .then(result => {
+ this.getChangeModel().updateStateChange(
+ GrReviewerUpdatesParser.parse(
+ result?.change_info as ChangeViewChangeInfo
+ )
+ );
this.patchsetLevelDraftMessage = '';
this.includeComments = true;
fireNoBubble(this, 'send', {});
fireIronAnnounce(this, 'Reply sent');
- return;
})
- .then(result => {
+ .finally(() => {
this.disabled = false;
- return result;
- })
- .catch(err => {
- this.disabled = false;
- throw err;
+ if (this.patchsetLevelGrComment) {
+ this.patchsetLevelGrComment.disableAutoSaving = false;
+ }
});
}
@@ -1836,7 +1843,8 @@
this.change._number,
this.latestPatchNum,
review,
- errFn
+ errFn,
+ true
);
}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 27b4cc8..3c3c246 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -21,7 +21,6 @@
DraftsAction,
ReviewerState,
} from '../../../constants/constants';
-import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {StandardLabels} from '../../../utils/label-util';
import {
createAccountWithEmail,
@@ -170,14 +169,7 @@
new Promise((resolve, reject) => {
try {
const result = jsonResponseProducer(review) || {};
- const resultStr = JSON_PREFIX + JSON.stringify(result);
- resolve({
- ...new Response(),
- ok: true,
- text() {
- return Promise.resolve(resultStr);
- },
- });
+ resolve(result);
} catch (err) {
reject(err);
}
@@ -1522,62 +1514,6 @@
assert.isTrue(element.reviewersMutated);
});
- test('400 converts to human-readable server-error', async () => {
- stubRestApi('saveChangeReview').callsFake(
- (_changeNum, _patchNum, _review, errFn) => {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
- errFn!(
- cloneableResponse(
- 400,
- '....{"reviewers":{"id1":{"error":"human readable"}}}'
- ) as Response
- );
- return Promise.resolve(new Response());
- }
- );
-
- const promise = mockPromise();
- const listener = (event: Event) => {
- if (event.target !== document) return;
- (event as CustomEvent).detail.response.text().then((body: string) => {
- if (body === 'human readable') {
- promise.resolve();
- }
- });
- };
- addListenerForTest(document, 'server-error', listener);
-
- await element.updateComplete;
- element.send(false, false);
- await promise;
- });
-
- test('non-json 400 is treated as a normal server-error', async () => {
- stubRestApi('saveChangeReview').callsFake(
- (_changeNum, _patchNum, _review, errFn) => {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
- errFn!(cloneableResponse(400, 'Comment validation error!') as Response);
- return Promise.resolve(new Response());
- }
- );
- const promise = mockPromise();
- const listener = (event: Event) => {
- if (event.target !== document) return;
- (event as CustomEvent).detail.response.text().then((body: string) => {
- if (body === 'Comment validation error!') {
- promise.resolve();
- }
- });
- };
- addListenerForTest(document, 'server-error', listener);
-
- // Async tick is needed because iron-selector content is distributed and
- // distributed content requires an observer to be set up.
- await element.updateComplete;
- element.send(false, false);
- await promise;
- });
-
test('filterReviewerSuggestion', () => {
const owner = makeAccount();
const reviewer1 = makeAccount();
@@ -2490,6 +2426,7 @@
await waitUntil(
() => element.patchsetLevelDraftMessage === 'hello world'
);
+ await element.updateComplete;
const saveReviewPromise = interceptSaveReview();
@@ -2499,7 +2436,7 @@
const review = await saveReviewPromise;
- assert.deepEqual(autoSaveStub.callCount, 1);
+ assert.deepEqual(autoSaveStub.callCount, 0);
assert.deepEqual(review, {
drafts: DraftsAction.PUBLISH_ALL_REVISIONS,
@@ -2514,6 +2451,65 @@
user: 999 as UserId,
},
],
+ comments: {
+ '/PATCHSET_LEVEL': [
+ {
+ message: 'hello world',
+ path: '/PATCHSET_LEVEL',
+ unresolved: false,
+ },
+ ],
+ },
+ remove_from_attention_set: [],
+ ignore_automatic_attention_set_rules: true,
+ });
+ });
+
+ test('sending waits for inflight autosave', async () => {
+ const patchsetLevelComment = queryAndAssert<GrComment>(
+ element,
+ '#patchsetLevelComment'
+ );
+
+ const waitForPendingDiffDrafts = stubRestApi(
+ 'awaitPendingDiffDrafts'
+ ).returns(Promise.resolve());
+
+ patchsetLevelComment.messageText = 'hello world';
+ await waitUntil(
+ () => element.patchsetLevelDraftMessage === 'hello world'
+ );
+ await element.updateComplete;
+
+ const saveReviewPromise = interceptSaveReview();
+
+ queryAndAssert<GrButton>(element, '.send').click();
+
+ const review = await saveReviewPromise;
+ assert.deepEqual(waitForPendingDiffDrafts.callCount, 1);
+
+ assert.deepEqual(review, {
+ drafts: DraftsAction.PUBLISH_ALL_REVISIONS,
+ labels: {
+ 'Code-Review': 0,
+ Verified: 0,
+ },
+ reviewers: [],
+ add_to_attention_set: [
+ {
+ reason: '<GERRIT_ACCOUNT_1> replied on the change',
+ user: 999 as UserId,
+ },
+ ],
+ comments: {
+ '/PATCHSET_LEVEL': [
+ {
+ message: 'hello world',
+ path: '/PATCHSET_LEVEL',
+ unresolved: false,
+ },
+ ],
+ },
remove_from_attention_set: [],
ignore_automatic_attention_set_rules: true,
});
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
index db49e8d..d35855d 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-icon/gr-icon';
+import '../../shared/gr-formatted-text/gr-formatted-text';
import {customElement, property} from 'lit/decorators.js';
import {css, html, LitElement} from 'lit';
import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 19f96a1..8372b41 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -23,7 +23,6 @@
iconForRun,
PRIMARY_STATUS_ACTIONS,
primaryRunAction,
- worstCategory,
} from '../../models/checks/checks-util';
import {
CheckRun,
@@ -366,7 +365,7 @@
*/
renderAdditionalIcon() {
if (this.run.status !== RunStatus.RUNNING) return nothing;
- const category = worstCategory(this.run);
+ const category = this.run.worstCategory;
if (!category) return nothing;
const icon = iconFor(category);
return html`
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
index 1999e1f..a0a3a44 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
@@ -55,6 +55,9 @@
fontStyles,
css`
.container {
+ /* Allows hiding the check results along with the comments
+ when the user presses the keyboard shortcut 'h'. */
+ display: var(--gr-comment-thread-display, block);
font-family: var(--font-family);
margin: 0 var(--spacing-s) var(--spacing-s);
background-color: var(--unresolved-comment-background-color);
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index 5c17dcd..d1ef25b 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -7,13 +7,12 @@
import {fontStyles} from '../../styles/gr-font-styles';
import {customElement, property} from 'lit/decorators.js';
import './gr-checks-action';
-import {CheckRun} from '../../models/checks/checks-model';
+import {CheckRun, RunResult} from '../../models/checks/checks-model';
import {
AttemptDetail,
ChecksIcon,
iconFor,
runActions,
- worstCategory,
} from '../../models/checks/checks-util';
import {durationString, fromNow} from '../../utils/date-util';
import {RunStatus} from '../../api/checks';
@@ -28,7 +27,7 @@
@customElement('gr-hovercard-run')
export class GrHovercardRun extends base {
@property({type: Object})
- run?: CheckRun;
+ run?: RunResult | CheckRun;
static override get styles() {
return [
@@ -357,8 +356,7 @@
computeIcon(): ChecksIcon {
if (!this.run) return {name: ''};
- const category = worstCategory(this.run);
- if (category) return iconFor(category);
+ if (this.run.worstCategory) return iconFor(this.run.worstCategory);
return this.run.status === RunStatus.COMPLETED
? iconFor(RunStatus.COMPLETED)
: {name: ''};
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
index 5510aa3..ca0480c9 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.ts
@@ -33,6 +33,9 @@
@property({type: String})
loginUrl = '/login';
+ @property({type: String})
+ loginText = 'Sign in';
+
@property({type: Boolean})
showSignInButton = false;
@@ -82,7 +85,7 @@
return html`
<gr-button id="signIn" class="signInLink" link="" slot="footer">
- <a class="signInLink" href=${this.loginUrl}>Sign in</a>
+ <a class="signInLink" href=${this.loginUrl}>${this.loginText}</a>
</gr-button>
`;
}
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index 570d393..c8a1c39 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -113,6 +113,9 @@
@property({type: String})
loginUrl = '/login';
+ @property({type: String})
+ loginText = 'Sign in';
+
private readonly reporting = getAppContext().reportingService;
private readonly getAuthService = resolve(this, authServiceToken);
@@ -164,6 +167,7 @@
id="errorDialog"
@dismiss=${() => this.errorModal.close()}
.loginUrl=${this.loginUrl}
+ .loginText=${this.loginText}
></gr-error-dialog>
</dialog>
<dialog
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index 8df56b0..c70e677 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -116,6 +116,9 @@
@property({type: String})
loginUrl = '/login';
+ @property({type: String})
+ loginText = 'Sign in';
+
@property({type: Boolean})
mobileSearchHidden = false;
@@ -446,7 +449,7 @@
></gr-icon>
</div>
${this.renderRegister()}
- <a class="loginButton" href=${this.loginUrl}>Sign in</a>
+ <a class="loginButton" href=${this.loginUrl}>${this.loginText}</a>
<a
class="settingsButton"
href="${getBaseUrl()}/settings/"
diff --git a/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt_test.ts b/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt_test.ts
index d06b405..cbbcee0 100644
--- a/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-notifications-prompt/gr-notifications-prompt_test.ts
@@ -33,10 +33,7 @@
allow_browser_notifications: true,
};
userModel.setPreferences(prefs);
- await waitUntilObserved(
- userModel.preferences$,
- pref => pref.allow_browser_notifications === true
- );
+
await waitUntilObserved(
userModel.preferences$,
pref => pref.allow_browser_notifications === true
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 864f559..bb67a40 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -102,6 +102,7 @@
import {
InteractivePromise,
interactivePromise,
+ noAwait,
timeoutPromise,
} from '../../../utils/async-util';
@@ -506,9 +507,8 @@
/** gr-page middleware that warms the REST API's logged-in cache line. */
private loadUserMiddleware(_: PageContext, next: PageNextCallback) {
- this.restApiService.getLoggedIn().then(() => {
- next();
- });
+ noAwait(this.restApiService.getLoggedIn());
+ next();
}
/**
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 02d5124..769b064 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
@@ -42,7 +42,7 @@
import {Timing} from '../../../constants/reporting';
import {changeModelToken} from '../../../models/change/change-model';
-interface FilePreview {
+export interface FilePreview {
filepath: string;
preview: DiffInfo;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index 00e013b..04a55fd 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -204,11 +204,11 @@
/**
* Get the comments (with drafts and robot comments) for a path and
- * patch-range. Returns an object with left and right properties mapping to
- * arrays of comments in on either side of the patch range for that path.
+ * patch-range. Returns an array containing comments from either side of the
+ * patch range for that path.
*
- * @param patchRange The patch-range object containing patchNum
- * and basePatchNum properties to represent the range.
+ * @param patchRange The patch-range object containing patchNum and
+ * basePatchNum properties to represent the range.
*/
getCommentsForPath(path: string, patchRange: PatchRange): Comment[] {
let comments: Comment[] = [];
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 914aa19..7efc72b 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
@@ -385,6 +385,11 @@
override connectedCallback() {
super.connectedCallback();
this.subscribeToChecks();
+ this.getPluginLoader().jsApiService.handleShowDiff({
+ change: this.change!,
+ fileRange: this.file!,
+ patchRange: this.patchRange!,
+ });
}
override disconnectedCallback() {
@@ -671,7 +676,23 @@
private getLayers(enableTokenHighlight: boolean): DiffLayer[] {
const layers = [];
if (enableTokenHighlight) {
- layers.push(new TokenHighlightLayer(this));
+ layers.push(
+ new TokenHighlightLayer(this, highlight => {
+ for (const plugin of this.getPluginLoader().pluginsModel.getState()
+ .tokenHighlightPlugins) {
+ plugin.listener(
+ {
+ change: this.change!,
+ basePatchNum: this.patchRange!.basePatchNum,
+ patchNum: this.patchRange!.patchNum,
+ fileRange: this.file!,
+ path: this.path!,
+ },
+ highlight
+ );
+ }
+ })
+ );
}
layers.push(this.syntaxLayer);
return layers;
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 df9a634..8f98497 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
@@ -69,11 +69,13 @@
setup(async () => {
stubRestApi('getAccount').callsFake(() => Promise.resolve(account));
- element = await fixture(html`<gr-diff-host></gr-diff-host>`);
- element.changeNum = 123 as NumericChangeId;
- element.path = 'some/path';
- element.change = createChange();
- element.patchRange = createPatchRange();
+ element = await fixture(html`<gr-diff-host
+ .changeNum=${123 as NumericChangeId}
+ .path=${'some/path'}
+ .file=${{path: 'some/path'}}
+ .change=${createChange()}
+ .patchRange=${createPatchRange()}
+ ></gr-diff-host>`);
getDiffRestApiStub = stubRestApi('getDiff');
// Fall back in case a test forgets to set one up
getDiffRestApiStub.returns(Promise.resolve(createDiff()));
@@ -606,82 +608,73 @@
assert.equal(stub.lastCall.args.length, 0);
});
- suite('blame', () => {
- setup(async () => {
- element = await fixture(html`<gr-diff-host></gr-diff-host>`);
- element.changeNum = 123 as NumericChangeId;
- element.path = 'some/path';
- await element.updateComplete;
- });
+ test('clearBlame', async () => {
+ element.blame = [];
+ await element.updateComplete;
+ assertIsDefined(element.diffElement);
+ const isBlameLoadedStub = sinon.stub();
+ element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub);
+ element.clearBlame();
+ await element.updateComplete;
+ assert.isNull(element.blame);
+ assert.isTrue(isBlameLoadedStub.calledOnce);
+ assert.isFalse(isBlameLoadedStub.args[0][0].detail.value);
+ });
- test('clearBlame', async () => {
- element.blame = [];
- await element.updateComplete;
- assertIsDefined(element.diffElement);
- const isBlameLoadedStub = sinon.stub();
- element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub);
- element.clearBlame();
- await element.updateComplete;
- assert.isNull(element.blame);
- assert.isTrue(isBlameLoadedStub.calledOnce);
- assert.isFalse(isBlameLoadedStub.args[0][0].detail.value);
- });
+ test('loadBlame', async () => {
+ const mockBlame: BlameInfo[] = [createBlame()];
+ const showAlertStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ const getBlameStub = stubRestApi('getBlame').returns(
+ Promise.resolve(mockBlame)
+ );
+ const changeNum = 42 as NumericChangeId;
+ element.changeNum = changeNum;
+ element.patchRange = createPatchRange();
+ element.path = 'foo/bar.baz';
+ await element.updateComplete;
+ const isBlameLoadedStub = sinon.stub();
+ element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub);
- test('loadBlame', async () => {
- const mockBlame: BlameInfo[] = [createBlame()];
- const showAlertStub = sinon.stub();
- element.addEventListener('show-alert', showAlertStub);
- const getBlameStub = stubRestApi('getBlame').returns(
- Promise.resolve(mockBlame)
+ return element.loadBlame().then(() => {
+ assert.isTrue(
+ getBlameStub.calledWithExactly(
+ changeNum,
+ 1 as RevisionPatchSetNum,
+ 'foo/bar.baz',
+ true
+ )
);
- const changeNum = 42 as NumericChangeId;
- element.changeNum = changeNum;
- element.patchRange = createPatchRange();
- element.path = 'foo/bar.baz';
- await element.updateComplete;
- const isBlameLoadedStub = sinon.stub();
- element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub);
+ assert.isFalse(showAlertStub.called);
+ assert.equal(element.blame, mockBlame);
+ assert.isTrue(isBlameLoadedStub.calledOnce);
+ assert.isTrue(isBlameLoadedStub.args[0][0].detail.value);
+ });
+ });
- return element.loadBlame().then(() => {
- assert.isTrue(
- getBlameStub.calledWithExactly(
- changeNum,
- 1 as RevisionPatchSetNum,
- 'foo/bar.baz',
- true
- )
- );
- assert.isFalse(showAlertStub.called);
- assert.equal(element.blame, mockBlame);
- assert.isTrue(isBlameLoadedStub.calledOnce);
- assert.isTrue(isBlameLoadedStub.args[0][0].detail.value);
+ test('loadBlame empty', async () => {
+ const mockBlame: BlameInfo[] = [];
+ const showAlertStub = sinon.stub();
+ const isBlameLoadedStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub);
+ stubRestApi('getBlame').returns(Promise.resolve(mockBlame));
+ const changeNum = 42 as NumericChangeId;
+ element.changeNum = changeNum;
+ element.patchRange = createPatchRange();
+ element.path = 'foo/bar.baz';
+ await element.updateComplete;
+ return element
+ .loadBlame()
+ .then(() => {
+ assert.isTrue(false, 'Promise should not resolve');
+ })
+ .catch(() => {
+ assert.isTrue(showAlertStub.calledOnce);
+ assert.isNull(element.blame);
+ // We don't expect a call because
+ assert.isTrue(isBlameLoadedStub.notCalled);
});
- });
-
- test('loadBlame empty', async () => {
- const mockBlame: BlameInfo[] = [];
- const showAlertStub = sinon.stub();
- const isBlameLoadedStub = sinon.stub();
- element.addEventListener('show-alert', showAlertStub);
- element.addEventListener('is-blame-loaded-changed', isBlameLoadedStub);
- stubRestApi('getBlame').returns(Promise.resolve(mockBlame));
- const changeNum = 42 as NumericChangeId;
- element.changeNum = changeNum;
- element.patchRange = createPatchRange();
- element.path = 'foo/bar.baz';
- await element.updateComplete;
- return element
- .loadBlame()
- .then(() => {
- assert.isTrue(false, 'Promise should not resolve');
- })
- .catch(() => {
- assert.isTrue(showAlertStub.calledOnce);
- assert.isNull(element.blame);
- // We don't expect a call because
- assert.isTrue(isBlameLoadedStub.notCalled);
- });
- });
});
test('delegates clearDiffContent()', () => {
@@ -769,12 +762,9 @@
let reportStub: SinonStubbedMember<ReportingService['reportInteraction']>;
setup(async () => {
- element = await fixture(html`<gr-diff-host></gr-diff-host>`);
- element.changeNum = 123 as NumericChangeId;
- element.path = 'file.txt';
element.patchRange = createPatchRange(1, 2);
- reportStub = sinon.stub(element.reporting, 'reportInteraction');
await element.updateComplete;
+ reportStub = sinon.stub(element.reporting, 'reportInteraction');
reportStub.reset();
});
@@ -1390,24 +1380,18 @@
];
setup(async () => {
- coverageProviderStub = sinon
- .stub()
- .returns(Promise.resolve(exampleRanges));
- element = await fixture(html`<gr-diff-host></gr-diff-host>`);
- element.changeNum = 123 as NumericChangeId;
- element.change = createChange();
- element.path = 'some/path';
- const prefs = {
+ element.prefs = {
...createDefaultDiffPrefs(),
line_length: 10,
show_tabs: true,
tab_size: 4,
context: -1,
};
- element.patchRange = createPatchRange();
- element.prefs = prefs;
await element.updateComplete;
+ coverageProviderStub = sinon
+ .stub()
+ .returns(Promise.resolve(exampleRanges));
getDiffRestApiStub.returns(
Promise.resolve({
...createDiff(),
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index 166444c..f228fb3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -7,6 +7,7 @@
import '@polymer/iron-a11y-announcer/iron-a11y-announcer';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icon/gr-icon';
+import '../../shared/gr-tooltip-content/gr-tooltip-content';
import {DiffViewMode} from '../../../constants/constants';
import {customElement, property, state} from 'lit/decorators.js';
import {fireIronAnnounce} from '../../../utils/event-util';
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 93cb124..8e6486e 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
@@ -5,6 +5,8 @@
*/
import '@polymer/iron-dropdown/iron-dropdown';
import '@polymer/iron-input/iron-input';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../../../styles/gr-a11y-styles';
import '../../../styles/shared-styles';
import '../../shared/gr-button/gr-button';
@@ -45,10 +47,11 @@
PreferencesInfo,
RepoName,
RevisionPatchSetNum,
+ Comment,
CommentMap,
} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo, WebLinkInfo} from '../../../types/diff';
-import {FileRange, ParsedChangeInfo} from '../../../types/types';
+import {ParsedChangeInfo} from '../../../types/types';
import {
FilesWebLinks,
PatchRangeChangeEvent,
@@ -70,7 +73,11 @@
ShortcutSection,
shortcutsServiceToken,
} from '../../../services/shortcuts/shortcuts-service';
-import {DisplayLine, LineSelectedEventDetail} from '../../../api/diff';
+import {
+ DisplayLine,
+ FileRange,
+ LineSelectedEventDetail,
+} from '../../../api/diff';
import {GrDownloadDialog} from '../../change/gr-download-dialog/gr-download-dialog';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
@@ -83,6 +90,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {ifDefined} from 'lit/directives/if-defined.js';
import {when} from 'lit/directives/when.js';
+import {styleMap} from 'lit/directives/style-map.js';
import {
createDiffUrl,
ChangeChildView,
@@ -139,6 +147,11 @@
@query('#diffPreferencesDialog')
diffPreferencesDialog?: GrDiffPreferencesDialog;
+ @query('.sidebarAnchor')
+ sidebarAnchor?: HTMLDivElement;
+
+ @state() private sidebarHeight = 0;
+
// Private but used in tests.
@state()
get patchRange(): PatchRange | undefined {
@@ -182,6 +195,8 @@
@state() path?: string;
+ @state() private shownSidebar?: string;
+
/** Allows us to react when the user switches to the DIFF view. */
// Private but used in tests.
@state() isActiveChildView = false;
@@ -222,6 +237,9 @@
@state()
leftSide = false;
+ @state()
+ commentsForPath: Comment[] = [];
+
// visible for testing
reviewedFiles = new Set<string>();
@@ -475,6 +493,7 @@
:host {
display: block;
background-color: var(--view-background-color);
+ --sidebar-width: 300px;
}
.hidden {
display: none;
@@ -489,9 +508,8 @@
background-color: var(--view-background-color);
position: sticky;
top: 0;
- /* TODO(dhruvsri): This is required only because of 'position:relative' in
- <gr-diff-highlight> (which could maybe be removed??). */
- z-index: 1;
+ /* sidebar should outrank <footer> in GrAppElement */
+ z-index: 110;
box-shadow: var(--elevation-level-1);
/* This is just for giving the box-shadow some space. */
margin-bottom: 2px;
@@ -650,6 +668,26 @@
:host(.hideComments) {
--gr-comment-thread-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);
+ padding: var(--spacing-l);
+ border: var(--spacing-xs) solid var(--border-color);
+ border-left: 0;
+ overflow: auto;
+ }
`,
];
}
@@ -663,10 +701,14 @@
// TODO(newdiff-cleanup): Remove once newdiff migration is completed.
this.cursor = isNewDiff() ? new GrDiffCursorNew() : new GrDiffCursor();
if (this.diffHost) this.reInitCursor();
+ window.addEventListener('scroll', this.updateSidebarHeight);
+ window.addEventListener('resize', this.updateSidebarHeight);
}
override disconnectedCallback() {
this.cursor?.dispose();
+ window.removeEventListener('scroll', this.updateSidebarHeight);
+ window.removeEventListener('resize', this.updateSidebarHeight);
super.disconnectedCallback();
}
@@ -676,6 +718,13 @@
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 (
@@ -720,6 +769,21 @@
});
}
}
+ if (
+ (changedProperties.has('change') ||
+ changedProperties.has('changeComments') ||
+ changedProperties.has('path') ||
+ changedProperties.has('patchRange')) &&
+ this.changeComments !== undefined &&
+ this.path !== undefined &&
+ this.patchRange !== undefined
+ ) {
+ this.commentsForPath = this.changeComments.getCommentsForPath(
+ this.path,
+ this.patchRange
+ );
+ }
+ this.updateSidebarHeight();
}
override render() {
@@ -731,25 +795,27 @@
return html`
${this.renderStickyHeader()}
<h2 class="assistive-tech-only">Diff view</h2>
- <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}
- @is-image-diff-changed=${this.onIsImageDiffChanged}
- @render=${this.reInitCursor}
- >
- </gr-diff-host>
+ <div class="diffContainer ${this.shownSidebar && 'sidebarOpen'}">
+ <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}
+ @is-image-diff-changed=${this.onIsImageDiffChanged}
+ @render=${this.reInitCursor}
+ >
+ </gr-diff-host>
+ </div>
${this.renderDialogs()}
`;
}
@@ -774,6 +840,7 @@
>></a
>
</div>
+ ${this.renderSidebarContent()}
</div>`;
}
@@ -843,6 +910,98 @@
</div>`;
}
+ private renderSidebarTriggers() {
+ return html`
+ <div class="sidebarTriggerContainer">
+ <gr-endpoint-decorator name="sidebarTrigger">
+ <gr-endpoint-param
+ name="onTrigger"
+ .value=${(pluginName: string) =>
+ (this.shownSidebar =
+ this.shownSidebar === pluginName ? undefined : pluginName)}
+ ></gr-endpoint-param>
+ <!-- params cannot start falsy, so the value must be wrapped -->
+ <gr-endpoint-param
+ name="openSidebar"
+ .value=${{name: this.shownSidebar}}
+ ></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ `;
+ }
+
+ 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`})}
+ >
+ <gr-endpoint-decorator
+ name=${`sidebarContent-${this.shownSidebar}`}
+ >
+ <gr-endpoint-param
+ name="change"
+ .value=${this.change}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="path"
+ .value=${this.path}
+ ></gr-endpoint-param>
+ <!-- current diff path and, in case of rename, previous path -->
+ <gr-endpoint-param
+ name="fileRange"
+ .value=${this.getFileRange()}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="basePatchNum"
+ .value=${this.basePatchNum}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="patchNum"
+ .value=${this.patchNum}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="content"
+ .value=${this.diff}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="cursor"
+ .value=${this.cursor}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="diff"
+ .value=${this.diffHost?.diffElement}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="comments"
+ .value=${this.commentsForPath}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="onClose"
+ .value=${(pluginName: string) => {
+ // Only close the sidebar if that particular sidebar is
+ // still open. An async onClose callback should not close a
+ // different sidebar.
+ this.shownSidebar =
+ this.shownSidebar === pluginName
+ ? undefined
+ : this.shownSidebar;
+ }}
+ >
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ `
+ )}
+ </div>
+ `;
+ }
+
private renderPatchRangeLeft() {
return html` <div class="patchRangeLeft">
<gr-patch-range-select
@@ -872,6 +1031,7 @@
this.isBlameLoaded && !this.isBlameLoading ? 'Hide blame' : 'Show blame';
const diffModeSelectorClass = !this.diff || this.diff.binary ? 'hide' : '';
return html` <div class="rightControls">
+ ${this.renderSidebarTriggers()}
<span class="blameLoader ${blameLoaderClass}">
<gr-button
link=""
@@ -985,6 +1145,10 @@
}
}
+ /**
+ * Returns the current file path and, if it was renamed in this change, the
+ * previous file path.
+ */
private getFileRange() {
if (!this.files || !this.path) return;
const fileInfo = this.files.changeFilesByPath[this.path];
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 c085953..9c1c3ce 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
@@ -270,6 +270,12 @@
</span>
</div>
<div class="rightControls">
+ <div class="sidebarTriggerContainer">
+ <gr-endpoint-decorator name="sidebarTrigger">
+ <gr-endpoint-param name="onTrigger"></gr-endpoint-param>
+ <gr-endpoint-param name="openSidebar"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
<span class="blameLoader show">
<gr-button
aria-disabled="false"
@@ -348,9 +354,12 @@
>
</a>
</div>
+ <div class="sidebarAnchor"></div>
</div>
<h2 class="assistive-tech-only">Diff view</h2>
- <gr-diff-host id="diffHost"> </gr-diff-host>
+ <div class="diffContainer">
+ <gr-diff-host id="diffHost"> </gr-diff-host>
+ </div>
<gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog>
<gr-diff-preferences-dialog id="diffPreferencesDialog">
</gr-diff-preferences-dialog>
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
index a8bc84c..87fe27f 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
@@ -14,7 +14,10 @@
import {customElement, state} from 'lit/decorators.js';
import {resolve} from '../../../models/dependency';
import {subscribe} from '../../lit/subscription-controller';
-import {documentationViewModelToken} from '../../../models/views/documentation';
+import {
+ createDocumentationUrl,
+ documentationViewModelToken,
+} from '../../../models/views/documentation';
@customElement('gr-documentation-search')
export class GrDocumentationSearch extends LitElement {
@@ -57,7 +60,7 @@
.filter=${this.filter}
.offset=${0}
.loading=${this.loading}
- .path=${'/Documentation'}
+ .path=${createDocumentationUrl()}
>
<table id="list" class="genericList">
<tbody>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index 5786112..c3b4e65 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -563,8 +563,6 @@
}
const fr = new FileReader();
- // TODO(TS): Do we need this line?
- // fr.file = file;
fr.onload = (fileLoadEvent: ProgressEvent<FileReader>) => {
if (!fileLoadEvent) return;
const fileData = fileLoadEvent.target!.result;
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index 824f4cb..647a2fb 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -28,11 +28,11 @@
import './settings/gr-registration-dialog/gr-registration-dialog';
import './settings/gr-settings-view/gr-settings-view';
import './core/gr-notifications-prompt/gr-notifications-prompt';
-import {getBaseUrl} from '../utils/url-util';
+import {loginUrl} from '../utils/url-util';
import {navigationToken} from './core/gr-navigation/gr-navigation';
import {getAppContext} from '../services/app-context';
import {routerToken} from './core/gr-router/gr-router';
-import {AccountDetailInfo, NumericChangeId} from '../types/common';
+import {AccountDetailInfo, NumericChangeId, ServerInfo} from '../types/common';
import {
constructServerErrorMsg,
GrErrorManager,
@@ -75,6 +75,7 @@
import {modalStyles} from '../styles/gr-modal-styles';
import {AdminChildView, createAdminUrl} from '../models/views/admin';
import {ChangeChildView, changeViewModelToken} from '../models/views/change';
+import {configModelToken} from '../models/config/config-model';
interface ErrorInfo {
text: string;
@@ -135,8 +136,6 @@
@state() private mobileSearch = false;
- @state() private loginUrl = '/login';
-
@state() private loadRegistrationDialog = false;
@state() private loadKeyboardShortcutsDialog = false;
@@ -153,6 +152,9 @@
@state() private theme = AppTheme.AUTO;
+ @state()
+ serverConfig?: ServerInfo;
+
readonly getRouter = resolve(this, routerToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -171,6 +173,8 @@
private readonly getChangeViewModel = resolve(this, changeViewModelToken);
+ private readonly getConfigModel = resolve(this, configModelToken);
+
constructor() {
super();
@@ -183,9 +187,7 @@
this.addEventListener('dialog-change', e => {
this.handleDialogChange(e as CustomEvent<DialogChangeEventDetail>);
});
- document.addEventListener('location-change', () =>
- this.handleLocationChange()
- );
+ document.addEventListener('location-change', () => this.requestUpdate());
document.addEventListener('gr-rpc-log', e => this.handleRpcLog(e));
this.shortcuts.addAbstract(Shortcut.OPEN_SHORTCUT_HELP_DIALOG, () =>
this.showKeyboardShortcuts()
@@ -220,6 +222,14 @@
subscribe(
this,
+ () => this.getConfigModel().serverConfig$,
+ config => {
+ this.serverConfig = config;
+ }
+ );
+
+ subscribe(
+ this,
() => this.getUserModel().preferenceTheme$,
theme => {
this.theme = theme;
@@ -259,7 +269,6 @@
const resizeObserver = this.getBrowserModel().observeWidth();
resizeObserver.observe(this);
- this.updateLoginUrl();
this.reporting.appStarted();
this.getRouter().start();
@@ -308,11 +317,11 @@
border-left: 0;
border-top: 0;
box-shadow: var(--header-box-shadow);
- /* Make sure the header is above the main content, to preserve box-shadow
- visibility. We need 2 here instead of 1, because dropdowns in the
+ /* Make sure the header is above the main content, to preserve
+ box-shadow visibility. We need 111 here 1, because dropdowns in the
header should be shown on top of the sticky diff header, which has a
- z-index of 1. */
- z-index: 2;
+ z-index of 110. */
+ z-index: 111;
}
footer {
background: var(
@@ -399,7 +408,8 @@
<gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
<gr-error-manager
id="errorManager"
- .loginUrl=${this.loginUrl}
+ .loginUrl=${loginUrl(this.serverConfig?.auth)}
+ .loginText=${this.serverConfig?.auth.login_text ?? 'Sign in'}
></gr-error-manager>
<gr-plugin-host id="plugins"></gr-plugin-host>
`;
@@ -410,11 +420,12 @@
return html`
<gr-main-header
id="mainHeader"
- .searchQuery=${(this.params as SearchViewState)?.query}
+ .searchQuery=${(this.params as SearchViewState)?.query ?? ''}
@mobile-search=${this.mobileSearchToggle}
@show-keyboard-shortcuts=${this.showKeyboardShortcuts}
.mobileSearchHidden=${!this.mobileSearch}
- .loginUrl=${this.loginUrl}
+ .loginUrl=${loginUrl(this.serverConfig?.auth)}
+ .loginText=${this.serverConfig?.auth.login_text ?? 'Sign in'}
?aria-hidden=${this.footerHeaderAriaHidden}
>
</gr-main-header>
@@ -690,35 +701,6 @@
}
}
- private handleLocationChange() {
- this.updateLoginUrl();
- }
-
- private updateLoginUrl() {
- const baseUrl = getBaseUrl();
- if (baseUrl) {
- // Strip the canonical path from the path since needing canonical in
- // the path is unneeded and breaks the url.
- this.loginUrl =
- baseUrl +
- '/login/' +
- encodeURIComponent(
- '/' +
- window.location.pathname.substring(baseUrl.length) +
- window.location.search +
- window.location.hash
- );
- } else {
- this.loginUrl =
- '/login/' +
- encodeURIComponent(
- window.location.pathname +
- window.location.search +
- window.location.hash
- );
- }
- }
-
// private but used in test
paramsChanged() {
const viewsToCheck = [GerritView.SEARCH, GerritView.DASHBOARD];
diff --git a/polygerrit-ui/app/elements/gr-app_test.ts b/polygerrit-ui/app/elements/gr-app_test.ts
index 05ee9ed..6b97c85 100644
--- a/polygerrit-ui/app/elements/gr-app_test.ts
+++ b/polygerrit-ui/app/elements/gr-app_test.ts
@@ -19,11 +19,12 @@
import {GrRouter, routerToken} from './core/gr-router/gr-router';
import {resolve} from '../models/dependency';
import {removeRequestDependencyListener} from '../test/common-test-setup';
+import {ReactiveElement} from 'lit';
suite('gr-app callback tests', () => {
- const handleLocationChangeSpy = sinon.spy(
- GrAppElement.prototype,
- <any>'handleLocationChange'
+ const requestUpdateStub = sinon.stub(
+ ReactiveElement.prototype,
+ 'requestUpdate'
);
const dispatchLocationChangeEventSpy = sinon.spy(
GrRouter.prototype,
@@ -34,9 +35,9 @@
await fixture<GrApp>(html`<gr-app id="app"></gr-app>`);
});
- test("handleLocationChange in gr-app-element is called after dispatching 'location-change' event in gr-router", () => {
+ test("requestUpdate in reactive-element is called after dispatching 'location-change' event in gr-router", () => {
dispatchLocationChangeEventSpy();
- assert.isTrue(handleLocationChangeSpy.calledOnce);
+ assert.isTrue(requestUpdateStub.calledOnce);
});
});
diff --git a/polygerrit-ui/app/elements/lit/incremental-repeat.ts b/polygerrit-ui/app/elements/lit/incremental-repeat.ts
index 695290c..bcfb682 100644
--- a/polygerrit-ui/app/elements/lit/incremental-repeat.ts
+++ b/polygerrit-ui/app/elements/lit/incremental-repeat.ts
@@ -17,18 +17,24 @@
initialCount: number;
targetFrameRate?: number;
startAt?: number;
- // TODO: targetFramerate
+ endAt?: number;
}
interface RepeatState<T> {
values: T[];
mapFn?: (val: T, idx: number) => unknown;
startAt: number;
+ endAt: number;
incrementAmount: number;
lastRenderedAt: number;
targetFrameRate: number;
}
+// This directive supports incrementally rendering a list of elements.
+// It only responds to updates to values (which forces a complete re-render) and
+// an update to endAt (which expands the list).
+// It currently does not support changes to mapFn, initialCount or startAt
+// unless values are also changed.
class IncrementalRepeat<T> extends AsyncDirective {
private children: {part: ChildPart; options: RepeatOptions<T>}[] = [];
@@ -36,11 +42,14 @@
private state!: RepeatState<T>;
+ // Will render from `options.startAt` to `options.endAt`, up to
+ // `options.initialCount` elements.
render(options: RepeatOptions<T>) {
- const values = options.values.slice(
- options.startAt ?? 0,
- (options.startAt ?? 0) + options.initialCount
- );
+ const start = options.startAt ?? 0;
+ const offset = start + options.initialCount;
+ const end =
+ options.endAt === undefined ? offset : Math.min(options.endAt, offset);
+ const values = options.values.slice(start, end);
if (options.mapFn) {
return values.map(options.mapFn);
}
@@ -57,6 +66,7 @@
values: options.values,
mapFn: options.mapFn,
startAt: options.initialCount,
+ endAt: options.endAt ?? options.values.length,
incrementAmount: options.initialCount,
lastRenderedAt: performance.now(),
targetFrameRate: options.targetFrameRate ?? 30,
@@ -66,7 +76,19 @@
);
} else {
this.updateParts();
+ // TODO: Deal with updates to startAt by removing children and then
+ // trimming the child where the new startAt falls into.
+ if ((options.endAt ?? options.values.length) >= this.state.endAt) {
+ this.state.endAt = options.endAt ?? options.values.length;
+ if (this.nextScheduledFrameWork) {
+ cancelAnimationFrame(this.nextScheduledFrameWork);
+ }
+ this.nextScheduledFrameWork = requestAnimationFrame(
+ this.animationFrameHandler
+ );
+ }
}
+ // Render the first initial count.
return this.render(options);
}
@@ -92,6 +114,10 @@
private nextScheduledFrameWork: number | undefined;
private animationFrameHandler = () => {
+ if (this.state.startAt >= this.state.endAt) {
+ this.nextScheduledFrameWork = undefined;
+ return;
+ }
const now = performance.now();
const frameRate = 1000 / (now - this.state.lastRenderedAt);
if (frameRate < this.state.targetFrameRate) {
@@ -109,13 +135,16 @@
values: this.state.values,
initialCount: this.state.incrementAmount,
startAt: this.state.startAt,
+ endAt: this.state.endAt,
});
this.state.startAt += this.state.incrementAmount;
- if (this.state.startAt < this.state.values.length) {
+ if (this.state.startAt < this.state.endAt) {
this.nextScheduledFrameWork = requestAnimationFrame(
this.animationFrameHandler
);
+ } else {
+ this.nextScheduledFrameWork = undefined;
}
};
}
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
index 747f3eb..598ae28 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
@@ -227,7 +227,7 @@
<span class="value">
<iron-autogrow-textarea
id="statusInput"
- .name=${'statusInput'}
+ .label=${'statusInput'}
?disabled=${this.saving}
maxlength="140"
.value=${this.account?.status}
@@ -341,6 +341,10 @@
});
}
+ delete() {
+ return this.restApiService.deleteAccount();
+ }
+
private maybeSetName() {
// Note that we are intentionally not acting on this._account.name being the
// empty string (which is falsy).
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index ca2d9b8..bd81828 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -23,6 +23,7 @@
import '../gr-menu-editor/gr-menu-editor';
import '../gr-ssh-editor/gr-ssh-editor';
import '../gr-watched-projects-editor/gr-watched-projects-editor';
+import '../../shared/gr-dialog/gr-dialog';
import {GrAccountInfo} from '../gr-account-info/gr-account-info';
import {GrWatchedProjectsEditor} from '../gr-watched-projects-editor/gr-watched-projects-editor';
import {GrGroupList} from '../gr-group-list/gr-group-list';
@@ -64,6 +65,9 @@
import {settingsViewModelToken} from '../../../models/views/settings';
import {areNotificationsEnabled} from '../../../utils/worker-util';
import {userModelToken} from '../../../models/user/user-model';
+import {modalStyles} from '../../../styles/gr-modal-styles';
+import {navigationToken} from '../../core/gr-navigation/gr-navigation';
+import {rootUrl} from '../../../utils/url-util';
const GERRIT_DOCS_BASE_URL =
'https://gerrit-review.googlesource.com/' + 'Documentation';
@@ -88,6 +92,9 @@
@query('#accountInfo', true) accountInfo!: GrAccountInfo;
+ @query('#confirm-account-deletion')
+ private deleteAccountConfirmationDialog?: HTMLDialogElement;
+
@query('#watchedProjectsEditor', true)
watchedProjectsEditor!: GrWatchedProjectsEditor;
@@ -191,6 +198,8 @@
@state() account?: AccountDetailInfo;
+ @state() isDeletingAccount = false;
+
// private but used in test
public _testOnly_loadingPromise?: Promise<void>;
@@ -203,6 +212,8 @@
private readonly getViewModel = resolve(this, settingsViewModelToken);
+ private readonly getNavigation = resolve(this, navigationToken);
+
constructor() {
super();
subscribe(
@@ -309,6 +320,7 @@
paperStyles,
fontStyles,
formStyles,
+ modalStyles,
menuPageStyles,
pageNavStyles,
css`
@@ -339,6 +351,13 @@
margin-bottom: var(--spacing-l);
margin-right: var(--spacing-l);
}
+ .delete-account-button {
+ margin-left: var(--spacing-l);
+ }
+ .confirm-account-deletion-main ul {
+ list-style: disc inside;
+ margin-left: var(--spacing-l);
+ }
`,
];
}
@@ -404,6 +423,32 @@
?disabled=${!this.accountInfoChanged}
>Save changes</gr-button
>
+ <gr-button
+ class="delete-account-button"
+ @click=${() => {
+ this.confirmDeleteAccount();
+ }}
+ >Delete Account</gr-button
+ >
+ <dialog id="confirm-account-deletion">
+ <gr-dialog
+ @cancel=${() => this.deleteAccountConfirmationDialog?.close()}
+ @confirm=${() => this.deleteAccount()}
+ .loading=${this.isDeletingAccount}
+ .loadingLabel=${'Deleting account'}
+ .confirmLabel=${'Delete account'}
+ >
+ <div class="confirm-account-deletion-header" slot="header">
+ Are you sure you wish to delete your account?
+ </div>
+ <div class="confirm-account-deletion-main" slot="main">
+ <ul>
+ <li>Deleting your account is not reversible.</li>
+ <li>Deleting your account will not delete your changes.</li>
+ </ul>
+ </div>
+ </gr-dialog>
+ </dialog>
</fieldset>
<h2
id="Preferences"
@@ -1200,6 +1245,18 @@
});
}
+ private confirmDeleteAccount() {
+ this.deleteAccountConfirmationDialog?.showModal();
+ }
+
+ private async deleteAccount() {
+ this.isDeletingAccount = true;
+ await this.accountInfo.delete();
+ this.isDeletingAccount = false;
+ this.deleteAccountConfirmationDialog?.close();
+ this.getNavigation().setUrl(rootUrl());
+ }
+
// private but used in test
getFilterDocsLink(docsBaseUrl?: string | null) {
let base = docsBaseUrl;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index 18c1244..d77d35c 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -166,6 +166,37 @@
>
Save changes
</gr-button>
+ <gr-button
+ aria-disabled="false"
+ class="delete-account-button"
+ role="button"
+ tabindex="0"
+ >
+ Delete Account
+ </gr-button>
+ <dialog id="confirm-account-deletion">
+ <gr-dialog role="dialog">
+ <div
+ class="confirm-account-deletion-header"
+ slot="header"
+ >
+ Are you sure you wish to delete your account?
+ </div>
+ <div
+ class="confirm-account-deletion-main"
+ slot="main"
+ >
+ <ul>
+ <li>
+ Deleting your account is not reversible.
+ </li>
+ <li>
+ Deleting your account will not delete your changes.
+ </li>
+ </ul>
+ </div>
+ </gr-dialog>
+ </dialog>
</fieldset>
<h2 id="Preferences">Preferences</h2>
<fieldset id="preferences">
@@ -416,7 +447,7 @@
>
Send verification
</gr-button>
- </fieldset>
+ </fieldset>
<h2 id="Groups">Groups</h2>
<fieldset><gr-group-list id="groupList"> </gr-group-list></fieldset>
<h2 id="Identities">Identities</h2>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts
index 7040285..8196201 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack.ts
@@ -4,10 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
import './gr-avatar';
+import '../gr-hovercard-account/gr-hovercard-account';
import {AccountInfo, ServerInfo} from '../../../types/common';
-import {LitElement, css, html} from 'lit';
+import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {
+ isDetailedAccount,
uniqueAccountId,
uniqueDefinedAvatar,
} from '../../../utils/account-util';
@@ -15,6 +17,9 @@
import {configModelToken} from '../../../models/config/config-model';
import {subscribe} from '../../lit/subscription-controller';
import {getDisplayName} from '../../../utils/display-name-util';
+import {accountsModelToken} from '../../../models/accounts-model/accounts-model';
+import {isDefined} from '../../../types/types';
+import {when} from 'lit/directives/when.js';
/**
* This elements draws stack of avatars overlapped with each other.
@@ -43,14 +48,22 @@
imageSize = 16;
/**
+ * Whether a hover-card should be shown for each avatar when hovered
+ */
+ @property({type: Boolean})
+ enableHover = false;
+
+ /**
* In gr-app, gr-account-chip is in charge of loading a full account, so
* avatars will be set. However, code-owners will create gr-avatars with a
* bare account-id. To enable fetching of those avatars, a flag is added to
- * gr-avatar that will disregard the absence of avatar urls.
+ * gr-avatar-stack that will fetch the accounts on demand
*/
@property({type: Boolean})
forceFetch = false;
+ private readonly getAccountsModel = resolve(this, accountsModelToken);
+
@state() config?: ServerInfo;
static override get styles() {
@@ -81,6 +94,24 @@
);
}
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('accounts')) {
+ if (
+ this.forceFetch &&
+ this.accounts.length > 0 &&
+ this.accounts.some(a => !isDetailedAccount(a))
+ ) {
+ Promise.all(
+ this.accounts.map(account =>
+ this.getAccountsModel().fillDetails(account)
+ )
+ ).then(accounts => {
+ this.accounts = accounts.filter(isDefined);
+ });
+ }
+ }
+ }
+
override render() {
const uniqueAvatarAccounts = this.forceFetch
? this.accounts.filter(uniqueAccountId)
@@ -98,11 +129,17 @@
return uniqueAvatarAccounts.map(
account =>
html`<gr-avatar
- .forceFetch=${this.forceFetch}
.account=${account}
.imageSize=${this.imageSize}
aria-label=${getDisplayName(this.config, account)}
>
+ ${when(
+ this.enableHover,
+ () =>
+ html`<gr-hovercard-account
+ .account=${account}
+ ></gr-hovercard-account>`
+ )}
</gr-avatar>`
);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack_test.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack_test.ts
index 08066bf..8c1c5c7 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar-stack_test.ts
@@ -12,6 +12,7 @@
import {fixture, html, assert} from '@open-wc/testing';
import {stubRestApi} from '../../../test/test-utils';
import {LitElement} from 'lit';
+import {Timestamp} from '../../../api/rest-api';
suite('gr-avatar tests', () => {
suite('config with avatars', () => {
@@ -82,6 +83,104 @@
}
});
+ test('renders avatars and hovercards', async () => {
+ const accounts = [];
+ for (let i = 0; i < 2; ++i) {
+ accounts.push({
+ ...createAccountWithId(i),
+ avatars: [
+ {
+ url: `https://a.b.c/photo${i}.jpg`,
+ height: 32,
+ width: 32,
+ },
+ ],
+ });
+ }
+ accounts.push({
+ ...createAccountWithId(2),
+ avatars: [
+ {
+ // Account with duplicate avatar will be skipped.
+ url: 'https://a.b.c/photo1.jpg',
+ height: 32,
+ width: 32,
+ },
+ ],
+ });
+
+ const element: LitElement = await fixture(
+ html`<gr-avatar-stack
+ .accounts=${accounts}
+ .imageSize=${32}
+ .enableHover=${true}
+ ></gr-avatar-stack>`
+ );
+ await element.updateComplete;
+
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `<gr-avatar
+ aria-label="0"
+ style='background-image: url("https://a.b.c/photo0.jpg");'
+ >
+ <gr-hovercard-account></gr-hovercard-account>
+ </gr-avatar>
+ <gr-avatar
+ aria-label="1"
+ style='background-image: url("https://a.b.c/photo1.jpg");'
+ >
+ <gr-hovercard-account></gr-hovercard-account>
+ </gr-avatar> `
+ );
+ // Verify that margins are set correctly.
+ const avatars = element.shadowRoot!.querySelectorAll('gr-avatar');
+ assert.strictEqual(avatars.length, 2);
+ assert.strictEqual(window.getComputedStyle(avatars[0]).marginLeft, '0px');
+ for (let i = 1; i < avatars.length; ++i) {
+ assert.strictEqual(
+ window.getComputedStyle(avatars[i]).marginLeft,
+ '-8px'
+ );
+ }
+ });
+
+ test('fetches account details. avatars', async () => {
+ const stub = stubRestApi('getAccountDetails').resolves({
+ ...createAccountWithId(1),
+ avatars: [
+ {
+ url: 'https://a.b.c/photo0.jpg',
+ height: 32,
+ width: 32,
+ },
+ ],
+ registered_on: '1234' as Timestamp,
+ });
+ const element: LitElement = await fixture(
+ html`<gr-avatar-stack
+ .accounts=${[{_account_id: 1}]}
+ .forceFetch=${true}
+ .imageSize=${32}
+ ></gr-avatar-stack>`
+ );
+ await element.updateComplete;
+
+ assert.equal(stub.called, true);
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `<gr-avatar
+ aria-label="1"
+ style='background-image: url("https://a.b.c/photo0.jpg");'
+ >
+ </gr-avatar>`
+ );
+ // Verify that margins are set correctly.
+ const avatars = element.shadowRoot!.querySelectorAll('gr-avatar');
+ assert.strictEqual(avatars.length, 1);
+ assert.strictEqual(window.getComputedStyle(avatars[0]).marginLeft, '0px');
+ });
+
test('renders many accounts fallback', async () => {
const accounts = [];
for (let i = 0; i < 5; ++i) {
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
index 8cfe2d0..1ea2a64 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
@@ -25,14 +25,6 @@
@state() private hasAvatars = false;
- // In gr-app, gr-account-chip is in charge of loading a full account, so
- // avatars will be set. However, code-owners will create gr-avatars with a
- // bare account-id. To enable fetching of those avatars, a flag is added to
- // gr-avatar that will disregard the absence of avatar urls.
-
- @property({type: Boolean})
- forceFetch = false;
-
private readonly restApiService = getAppContext().restApiService;
private readonly getPluginLoader = resolve(this, pluginLoaderToken);
@@ -98,7 +90,7 @@
const avatars = account.avatars || [];
// if there is no avatar url in account, there is no avatar set on server,
// and request /avatar?s will be 404.
- if (avatars.length === 0 && !this.forceFetch) {
+ if (avatars.length === 0) {
return '';
}
for (let i = 0; i < avatars.length; i++) {
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index b44a16b..49d3c7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -3,6 +3,7 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import '../gr-icon/gr-icon';
import '@polymer/paper-button/paper-button';
import {spinnerStyles} from '../../../styles/gr-spinner-styles';
import {votingStyles} from '../../../styles/gr-voting-styles';
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
index 54fb825..d00d54f 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
@@ -3,6 +3,7 @@
* Copyright 2015 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import '../gr-icon/gr-icon';
import {ChangeInfo} from '../../../types/common';
import {
Shortcut,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts b/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
new file mode 100644
index 0000000..974329a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {Observable} from 'rxjs';
+import {filter} from 'rxjs/operators';
+import {define} from '../../../models/dependency';
+import {Model} from '../../../models/model';
+import {isDefined} from '../../../types/types';
+import {select} from '../../../utils/observable-util';
+import {Comment} from '../../../types/common';
+
+export interface CommentState {
+ comment: Comment;
+ commentedText?: string;
+}
+
+export const commentModelToken = define<CommentModel>('diff-model');
+
+export class CommentModel extends Model<CommentState | undefined> {
+ readonly comment$: Observable<Comment> = select(
+ this.state$.pipe(filter(isDefined)),
+ commentState => commentState.comment
+ );
+
+ readonly commentedText$: Observable<string | undefined> = select(
+ this.state$.pipe(filter(isDefined)),
+ commentState => commentState.commentedText
+ );
+}
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 30576c8..24777fb 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
@@ -315,6 +315,8 @@
font-size: var(--font-size-normal);
font-weight: var(--font-weight-normal);
line-height: var(--line-height-normal);
+ }
+ gr-diff#diff {
/* Explicitly set the background color of the diff. We
* cannot use the diff content type ab because of the skip chunk preceding
* it, diff processor assumes the chunk of type skip/ab can be collapsed
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 1d2b39e..d27be9f 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -17,7 +17,7 @@
import {getAppContext} from '../../../services/app-context';
import {css, html, LitElement, nothing, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
-import {resolve} from '../../../models/dependency';
+import {provide, resolve} from '../../../models/dependency';
import {GrTextarea} from '../gr-textarea/gr-textarea';
import {
AccountDetailInfo,
@@ -31,9 +31,11 @@
isError,
isDraft,
isNew,
+ CommentInput,
} from '../../../types/common';
import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
import {
+ convertToCommentInput,
createUserFixSuggestion,
getContentInCommentRange,
getUserSuggestion,
@@ -64,6 +66,12 @@
import {createDiffUrl} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {modalStyles} from '../../../styles/gr-modal-styles';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
+import {
+ CommentModel,
+ commentModelToken,
+} from '../gr-comment-model/gr-comment-model';
// visible for testing
export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
@@ -167,6 +175,10 @@
@property({type: Boolean, attribute: 'permanent-editing-mode'})
permanentEditingMode = false;
+ // Whether to disable autosaving
+ @property({type: Boolean})
+ disableAutoSaving = false;
+
@state()
autoSaving?: Promise<DraftInfo>;
@@ -212,8 +224,14 @@
private readonly getUserModel = resolve(this, userModelToken);
+ private readonly getPluginLoader = resolve(this, pluginLoaderToken);
+
+ private readonly flagsService = getAppContext().flagsService;
+
private readonly shortcuts = new ShortcutController(this);
+ private commentModel = new CommentModel(undefined);
+
/**
* This is triggered when the user types into the editing textarea. We then
* debounce it and call autoSave().
@@ -234,6 +252,7 @@
constructor() {
super();
+ provide(this, commentModelToken, () => this.commentModel);
// Allow the shortcuts to bubble up so that GrReplyDialog can respond to
// them as well.
this.shortcuts.addLocal({key: Key.ESC}, () => this.handleEsc(), {
@@ -767,8 +786,8 @@
return html`
<div class="rightActions">
${this.renderDiscardButton()} ${this.renderEditButton()}
- ${this.renderCancelButton()} ${this.renderSaveButton()}
- ${this.renderCopyLinkIcon()}
+ ${this.renderGenerateSuggestEditButton()} ${this.renderCancelButton()}
+ ${this.renderSaveButton()} ${this.renderCopyLinkIcon()}
</div>
`;
}
@@ -841,6 +860,38 @@
`;
}
+ // TODO(milutin): This is temporary solution for experimenting
+ private renderGenerateSuggestEditButton() {
+ if (!this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT)) {
+ return nothing;
+ }
+ return html`
+ <gr-button link class="action" @click=${this.generateSuggestEdit}
+ >Suggestion</gr-button
+ >
+ `;
+ }
+
+ // TODO(milutin): This is temporary solution for experimenting
+ private async generateSuggestEdit() {
+ const suggestionsPlugins =
+ this.getPluginLoader().pluginsModel.getState().suggestionsPlugins;
+ if (suggestionsPlugins.length === 0) return;
+ if (!this.changeNum || !this.comment?.patch_set || !this.comments?.[0].path)
+ return;
+ const suggestion = await suggestionsPlugins[0].provider.suggestCode({
+ prompt: this.messageText,
+ changeNumber: this.changeNum,
+ patchsetNumber: this.comment?.patch_set,
+ filePath: this.comments?.[0].path,
+ range: this.comments?.[0].range,
+ lineNumber: this.comments?.[0].line,
+ });
+ const replacement = suggestion.suggestions?.[0].replacement;
+ if (!replacement) return;
+ this.messageText += `${USER_SUGGESTION_START_PATTERN}${suggestion}${'\n```'}`;
+ }
+
private renderRobotActions() {
if (!this.account || !isRobot(this.comment)) return;
const endpoint = html`
@@ -935,6 +986,22 @@
whenVisible(this, () => this.textarea?.putCursorAtEnd());
}
}
+ if (changed.has('changeNum') || changed.has('comment')) {
+ if (
+ !this.flagsService.isEnabled(
+ KnownExperimentId.DIFF_FOR_USER_SUGGESTED_EDIT
+ ) ||
+ !this.changeNum ||
+ !this.comment
+ )
+ return;
+ (async () => {
+ const commentedText = await this.getCommentedCode();
+ this.commentModel.updateState({
+ commentedText,
+ });
+ })();
+ }
}
override willUpdate(changed: PropertyValues) {
@@ -943,6 +1010,11 @@
if (isDraft(this.comment) && isError(this.comment)) {
this.edit();
}
+ if (this.comment) {
+ this.commentModel.updateState({
+ comment: this.comment,
+ });
+ }
}
if (changed.has('editing')) {
this.onEditingChanged();
@@ -1149,6 +1221,7 @@
async autoSave() {
if (isSaving(this.comment) || this.autoSaving) return;
if (!this.editing || !this.comment) return;
+ if (this.disableAutoSaving) return;
assert(isDraft(this.comment), 'only drafts are editable');
const messageToSave = this.messageText.trimEnd();
if (messageToSave === '') return;
@@ -1167,6 +1240,15 @@
await this.save();
}
+ convertToCommentInput(): CommentInput | undefined {
+ if (!this.somethingToSave() || !this.comment) return;
+ return convertToCommentInput({
+ ...this.comment,
+ message: this.messageText.trimEnd(),
+ unresolved: this.unresolved,
+ });
+ }
+
async save() {
assert(isDraft(this.comment), 'only drafts are editable');
// There is a minimal chance of `isSaving()` being false between iterations
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 ec15e12..e6e27bb 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
@@ -6,6 +6,7 @@
import '@polymer/iron-input/iron-input';
import '../gr-button/gr-button';
import '../gr-icon/gr-icon';
+import '../gr-tooltip-content/gr-tooltip-content';
import {
assertIsDefined,
copyToClipbard,
@@ -106,7 +107,6 @@
id="input"
is="iron-input"
class=${classMap({hideInput: this.hideInput})}
- type="text"
@click=${this._handleInputClick}
readonly=""
.value=${this.text ?? ''}
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index 9b74e24..b6af123 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -171,7 +171,7 @@
}
@media only screen and (max-width: 50em) {
gr-select {
- display: var(--gr-select-style-display, inline);
+ display: var(--gr-select-style-display, inline-block);
width: var(--gr-select-style-width);
}
gr-button,
@@ -187,7 +187,7 @@
}
protected override willUpdate(changedProperties: PropertyValues): void {
- if (changedProperties.has('items') || changedProperties.has('value')) {
+ if (changedProperties.has('value')) {
this.handleValueChange();
}
}
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 c6bfb74..e810637 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
@@ -126,7 +126,7 @@
this.repoCommentLinks = repoCommentLinks;
// Always linkify URLs starting with https?://
this.repoCommentLinks['ALWAYS_LINK_HTTP'] = {
- match: '(https?://\\S+[\\w/])',
+ match: '(https?://\\S+[\\w/~-])',
link: '$1',
enabled: true,
};
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index 7bc0b11..88866d4 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -14,11 +14,15 @@
import {getAppContext} from '../../../services/app-context';
import './gr-formatted-text';
import {GrFormattedText} from './gr-formatted-text';
-import {createConfig} from '../../../test/test-data-generators';
+import {createComment, createConfig} from '../../../test/test-data-generators';
import {queryAndAssert, waitUntilObserved} from '../../../test/test-utils';
import {CommentLinks, EmailAddress} from '../../../api/rest-api';
import {testResolver} from '../../../test/common-test-setup';
import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
+import {
+ CommentModel,
+ commentModelToken,
+} from '../gr-comment-model/gr-comment-model';
suite('gr-formatted-text tests', () => {
let element: GrFormattedText;
@@ -37,6 +41,7 @@
testResolver(changeModelToken),
getAppContext().restApiService
);
+ const commentModel = new CommentModel({comment: createComment()});
await setCommentLinks({
customLinkRewrite: {
match: '(LinkRewriteMe)',
@@ -54,9 +59,13 @@
element = (
await fixture(
wrapInProvider(
- html`<gr-formatted-text></gr-formatted-text>`,
- configModelToken,
- configModel
+ wrapInProvider(
+ html`<gr-formatted-text></gr-formatted-text>`,
+ configModelToken,
+ configModel
+ ),
+ commentModelToken,
+ commentModel
)
)
).querySelector('gr-formatted-text')!;
@@ -210,35 +219,19 @@
});
test('does default linking', async () => {
- element.content = 'http://www.google.com';
- await element.updateComplete;
- assert.shadowDom.equal(
- element,
- /* HTML*/ `
- <pre class="plaintext">
- <a
- href="http://www.google.com"
- rel="noopener noreferrer"
- target="_blank"
- >http://www.google.com</a>
- </pre>
- `
- );
+ const checkLinking = async (url: string) => {
+ element.content = url;
+ await element.updateComplete;
+ const a = queryAndAssert<HTMLElement>(element, 'a');
+ assert.equal(a.getAttribute('href'), url);
+ assert.equal(a.innerText, url);
+ };
- element.content = 'https://www.google.com';
- await element.updateComplete;
- assert.shadowDom.equal(
- element,
- /* HTML*/ `
- <pre class="plaintext">
- <a
- href="https://www.google.com"
- rel="noopener noreferrer"
- target="_blank"
- >https://www.google.com</a>
- </pre>
- `
- );
+ await checkLinking('http://www.google.com');
+ await checkLinking('https://www.google.com');
+ await checkLinking('https://www.google.com/');
+ await checkLinking('https://www.google.com/asdf~');
+ await checkLinking('https://www.google.com/asdf-');
});
});
@@ -678,43 +671,18 @@
});
test('does default linking', async () => {
- element.content = 'http://www.google.com';
- await element.updateComplete;
- assert.shadowDom.equal(
- element,
- /* HTML*/ `
- <marked-element>
- <div slot="markdown-html" class="markdown-html">
- <p>
- <a
- href="http://www.google.com"
- rel="noopener noreferrer"
- target="_blank"
- >http://www.google.com</a>
- </p>
- </div>
- </marked-element>
- `
- );
+ const checkLinking = async (url: string) => {
+ element.content = url;
+ await element.updateComplete;
+ const a = queryAndAssert<HTMLElement>(element, 'a');
+ const p = queryAndAssert<HTMLElement>(element, 'p');
+ assert.equal(a.getAttribute('href'), url);
+ assert.equal(p.innerText, url);
+ };
- element.content = 'https://www.google.com';
- await element.updateComplete;
- assert.shadowDom.equal(
- element,
- /* HTML*/ `
- <marked-element>
- <div slot="markdown-html" class="markdown-html">
- <p>
- <a
- href="https://www.google.com"
- rel="noopener noreferrer"
- target="_blank"
- >https://www.google.com</a>
- </p>
- </div>
- </marked-element>
- `
- );
+ await checkLinking('http://www.google.com');
+ await checkLinking('https://www.google.com');
+ await checkLinking('https://www.google.com/');
});
suite('user suggest fix', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
index c2b6a33..ac6fa57 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents.ts
@@ -438,7 +438,7 @@
this.restApiService
.saveChangeReview(this.change._number, CURRENT, reviewInput)
.then(response => {
- if (!response || !response.ok) {
+ if (!response) {
throw new Error(
'something went wrong when toggling' +
this.getReviewerState(this.change!)
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
index 7df06f4..c3c48cb 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account-contents_test.ts
@@ -229,7 +229,7 @@
};
await element.updateComplete;
const saveReviewStub = stubRestApi('saveChangeReview').returns(
- Promise.resolve({...new Response(), ok: true})
+ Promise.resolve({})
);
stubRestApi('removeChangeReviewer').returns(
Promise.resolve({...new Response(), ok: true})
@@ -257,7 +257,7 @@
};
await element.updateComplete;
const saveReviewStub = stubRestApi('saveChangeReview').returns(
- Promise.resolve({...new Response(), ok: true})
+ Promise.resolve({})
);
stubRestApi('removeChangeReviewer').returns(
Promise.resolve({...new Response(), ok: true})
diff --git a/polygerrit-ui/app/elements/shared/gr-icon/gr-icon.ts b/polygerrit-ui/app/elements/shared/gr-icon/gr-icon.ts
index a2d70bc3..7e716b6 100644
--- a/polygerrit-ui/app/elements/shared/gr-icon/gr-icon.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icon/gr-icon.ts
@@ -49,6 +49,7 @@
white-space: nowrap;
word-wrap: normal;
direction: ltr;
+ font-feature-settings: 'liga';
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
font-variation-settings: 'FILL' 0;
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 4b5913c..0250d82 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
@@ -3,7 +3,11 @@
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {AnnotationPluginApi, CoverageProvider} from '../../../api/annotation';
+import {
+ AnnotationPluginApi,
+ CoverageProvider,
+ TokenHoverListener,
+} from '../../../api/annotation';
import {PluginApi} from '../../../api/plugin';
import {PluginsModel} from '../../../models/plugins/plugins-model';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
@@ -24,4 +28,12 @@
provider,
});
}
+
+ addTokenHoverListener(listener: TokenHoverListener): void {
+ this.reporting.trackApi(this.plugin, 'annotation', 'addTokenHoverListener');
+ this.pluginsModel.tokenHoverListenerRegister({
+ pluginName: this.plugin.getPluginName(),
+ listener,
+ });
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index 46c759b..cd73727 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -16,6 +16,7 @@
JsApiService,
EventCallback,
ShowChangeDetail,
+ ShowDiffDetail,
ShowRevisionActionsDetail,
} from './gr-js-api-types';
import {EventType, TargetElement} from '../../../api/plugin';
@@ -240,6 +241,21 @@
return review;
}
+ async handleShowDiff(detail: ShowDiffDetail): Promise<void> {
+ await this.waitForPluginsToLoad();
+ for (const cb of this._getEventCallbacks(EventType.SHOW_DIFF)) {
+ try {
+ cb(detail.change, detail.patchRange, detail.fileRange);
+ } catch (err: unknown) {
+ this.reporting.error(
+ 'GrJsApiInterface',
+ new Error('showDiff callback error'),
+ err
+ );
+ }
+ }
+ }
+
_getEventCallbacks(type: EventType) {
return eventCallbacks[type] || [];
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
index 8e3a87d..afa16b9 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-types.ts
@@ -15,6 +15,7 @@
import {EventType, TargetElement} from '../../../api/plugin';
import {ParsedChangeInfo} from '../../../types/types';
import {MenuLink} from '../../../api/admin';
+import {FileRange, PatchRange} from '../../../api/diff';
export interface ShowChangeDetail {
change?: ParsedChangeInfo;
@@ -23,6 +24,12 @@
info: {mergeable: boolean | null};
}
+export interface ShowDiffDetail {
+ change: ChangeInfo;
+ patchRange: PatchRange;
+ fileRange: FileRange;
+}
+
export interface ShowRevisionActionsDetail {
change: ChangeInfo;
revisionActions: {[key: string]: ActionInfo | undefined};
@@ -52,4 +59,5 @@
handleCommitMessage(change: ChangeInfo | ParsedChangeInfo, msg: string): void;
canSubmitChange(change: ChangeInfo, revision?: RevisionInfo | null): boolean;
getReviewPostRevert(change?: ChangeInfo): ReviewInput;
+ handleShowDiff(detail: ShowDiffDetail): void;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
index b1b66ad..fa50c53 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
@@ -138,6 +138,8 @@
* endpoint.
*/
getDetails(name: string): ModuleInfo[] {
- return this._endpoints.get(name) ?? [];
+ return (this._endpoints.get(name) ?? []).sort((m1, m2) =>
+ m1.plugin.getPluginName().localeCompare(m2.plugin.getPluginName())
+ );
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
index ea45142..14b5d14 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
@@ -47,6 +47,7 @@
@property({type: Boolean})
loading?: boolean;
+ /** Must include the base path. */
@property({type: String})
path?: string;
@@ -191,7 +192,8 @@
// Offset could be a string when passed from the router.
const offset = +(this.offset || 0);
const newOffset = Math.max(0, offset + this.itemsPerPage * direction);
- let href = getBaseUrl() + (this.path ?? '');
+ // Note that `this.path` already includes the base URL, if set and non-empty;
+ let href = this.path || getBaseUrl();
if (this.filter) {
href += '/q/filter:' + encodeURL(this.filter);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
index 38f4f17..5b1e162 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
@@ -61,23 +61,29 @@
element.offset = 25;
element.itemsPerPage = 25;
element.filter = 'test';
- element.path = '/admin/projects';
+ element.path = '/base/admin/projects';
- stubBaseUrl('');
+ stubBaseUrl('/base');
- assert.equal(element.computeNavLink(1), '/admin/projects/q/filter:test,50');
+ assert.equal(
+ element.computeNavLink(1),
+ '/base/admin/projects/q/filter:test,50'
+ );
- assert.equal(element.computeNavLink(-1), '/admin/projects/q/filter:test');
+ assert.equal(
+ element.computeNavLink(-1),
+ '/base/admin/projects/q/filter:test'
+ );
element.filter = undefined;
- assert.equal(element.computeNavLink(1), '/admin/projects,50');
+ assert.equal(element.computeNavLink(1), '/base/admin/projects,50');
- assert.equal(element.computeNavLink(-1), '/admin/projects');
+ assert.equal(element.computeNavLink(-1), '/base/admin/projects');
element.filter = 'plugins/';
assert.equal(
element.computeNavLink(1),
- '/admin/projects/q/filter:plugins/,50'
+ '/base/admin/projects/q/filter:plugins/,50'
);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.ts
index 1adc827..354a6b6 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.ts
@@ -82,8 +82,9 @@
/**
* Removes messages that describe removed reviewers, since reviewer_updates
* are used.
+ * Private but used in tests.
*/
- private _filterRemovedMessages() {
+ _filterRemovedMessages() {
this.result.messages = this.result.messages.filter(
message => message.tag !== MessageTag.TAG_DELETE_REVIEWER
);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js
index 4225173..b875441 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js
@@ -9,8 +9,6 @@
import {assert} from '@open-wc/testing';
suite('gr-reviewer-updates-parser tests', () => {
- let instance;
-
test('ignores changes without messages', () => {
const change = {};
sinon.stub(
@@ -80,7 +78,7 @@
},
],
};
- instance = new GrReviewerUpdatesParser(change);
+ const instance = new GrReviewerUpdatesParser(change);
instance._filterRemovedMessages();
assert.deepEqual(instance.result, {
messages: [{
@@ -122,7 +120,7 @@
],
};
- instance = new GrReviewerUpdatesParser(change);
+ const instance = new GrReviewerUpdatesParser(change);
instance._groupUpdates();
change = instance.result;
@@ -202,7 +200,7 @@
],
};
- instance = new GrReviewerUpdatesParser(change);
+ const instance = new GrReviewerUpdatesParser(change);
instance._formatUpdates();
assert.equal(change.reviewer_updates.length, 2);
@@ -264,7 +262,7 @@
message: 'Uploaded patch set 2.',
}],
};
- instance = new GrReviewerUpdatesParser(change);
+ const instance = new GrReviewerUpdatesParser(change);
instance._advanceUpdates();
const updates = instance.result.reviewer_updates;
assert.isBelow(parseDate(updates[0].date).getTime(), T0);
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
index 2b1ad9e..5e67a88b 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
@@ -3,9 +3,34 @@
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {css, html, LitElement, nothing} from 'lit';
-import {customElement} from 'lit/decorators.js';
+import '../../../embed/diff/gr-diff/gr-diff';
+import '../../shared/gr-button/gr-button';
+import '../../shared/gr-icon/gr-icon';
+import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {customElement, state} from 'lit/decorators.js';
+import {getAppContext} from '../../../services/app-context';
+import {Comment} from '../../../types/common';
import {fire} from '../../../utils/event-util';
+import {anyLineTooLong} from '../../../utils/diff-util';
+import {
+ DiffLayer,
+ DiffPreferencesInfo,
+ DiffViewMode,
+ RenderPreferences,
+} from '../../../api/diff';
+import {when} from 'lit/directives/when.js';
+import {GrSyntaxLayerWorker} from '../../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
+import {resolve} from '../../../models/dependency';
+import {highlightServiceToken} from '../../../services/highlight/highlight-service';
+import {NumericChangeId} from '../../../api/rest-api';
+import {changeModelToken} from '../../../models/change/change-model';
+import {subscribe} from '../../lit/subscription-controller';
+import {FilePreview} from '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
+import {userModelToken} from '../../../models/user/user-model';
+import {createUserFixSuggestion} from '../../../utils/comment-util';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {commentModelToken} from '../gr-comment-model/gr-comment-model';
declare global {
interface HTMLElementEventMap {
@@ -20,7 +45,78 @@
}
@customElement('gr-user-suggestion-fix')
-export class GrUserSuggetionFix extends LitElement {
+export class GrUserSuggestionsFix extends LitElement {
+ @state()
+ comment?: Comment;
+
+ @state()
+ commentedText?: string;
+
+ @state()
+ layers: DiffLayer[] = [];
+
+ @state()
+ previewLoaded = false;
+
+ @state()
+ changeNum?: NumericChangeId;
+
+ @state()
+ preview?: FilePreview;
+
+ @state()
+ diffPrefs?: DiffPreferencesInfo;
+
+ @state()
+ renderPrefs: RenderPreferences = {
+ disable_context_control_buttons: true,
+ show_file_comment_button: false,
+ hide_line_length_indicator: true,
+ };
+
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly getUserModel = resolve(this, userModelToken);
+
+ private readonly getCommentModel = resolve(this, commentModelToken);
+
+ private readonly flagsService = getAppContext().flagsService;
+
+ private readonly syntaxLayer = new GrSyntaxLayerWorker(
+ resolve(this, highlightServiceToken),
+ () => getAppContext().reportingService
+ );
+
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getChangeModel().changeNum$,
+ changeNum => (this.changeNum = changeNum)
+ );
+ subscribe(
+ this,
+ () => this.getUserModel().diffPreferences$,
+ diffPreferences => {
+ if (!diffPreferences) return;
+ this.diffPrefs = diffPreferences;
+ this.syntaxLayer.setEnabled(!!this.diffPrefs.syntax_highlighting);
+ }
+ );
+ subscribe(
+ this,
+ () => this.getCommentModel().comment$,
+ comment => (this.comment = comment)
+ );
+ subscribe(
+ this,
+ () => this.getCommentModel().commentedText$,
+ commentedText => (this.commentedText = commentedText)
+ );
+ }
+
static override get styles() {
return [
css`
@@ -60,6 +156,19 @@
];
}
+ override updated(changed: PropertyValues) {
+ if (changed.has('commentedText') || changed.has('comment')) {
+ if (
+ this.flagsService.isEnabled(
+ KnownExperimentId.DIFF_FOR_USER_SUGGESTED_EDIT
+ ) &&
+ !this.previewLoaded
+ ) {
+ this.fetchFixPreview();
+ }
+ }
+ }
+
override render() {
if (!this.textContent) return nothing;
const code = this.textContent;
@@ -92,17 +201,79 @@
</gr-button>
</div>
</div>
- <code>${code}</code>`;
+ ${when(
+ this.previewLoaded,
+ () => this.renderDiff(),
+ () => html`<code>${code}</code>`
+ )} `;
}
handleShowFix() {
if (!this.textContent) return;
fire(this, 'open-user-suggest-preview', {code: this.textContent});
}
+
+ private renderDiff() {
+ if (!this.preview) return;
+ const diff = this.preview.preview;
+ if (!anyLineTooLong(diff)) {
+ this.syntaxLayer.process(diff);
+ }
+ return html`<gr-diff
+ .prefs=${this.overridePartialDiffPrefs()}
+ .path=${this.preview.filepath}
+ .diff=${diff}
+ .layers=${this.layers}
+ .renderPrefs=${this.renderPrefs}
+ .viewMode=${DiffViewMode.UNIFIED}
+ ></gr-diff>`;
+ }
+
+ private async fetchFixPreview() {
+ if (
+ !this.changeNum ||
+ !this.comment?.patch_set ||
+ !this.textContent ||
+ !this.commentedText
+ )
+ return;
+
+ const fixSuggestions = createUserFixSuggestion(
+ this.comment,
+ this.commentedText,
+ this.textContent
+ );
+ const res = await this.restApiService.getFixPreview(
+ this.changeNum,
+ this.comment?.patch_set,
+ fixSuggestions[0].replacements
+ );
+ if (res) {
+ const currentPreviews = Object.keys(res).map(key => {
+ return {filepath: key, preview: res[key]};
+ });
+ if (currentPreviews.length > 0) {
+ this.preview = currentPreviews[0];
+ this.previewLoaded = true;
+ }
+ }
+
+ return res;
+ }
+
+ private overridePartialDiffPrefs() {
+ if (!this.diffPrefs) return undefined;
+ return {
+ ...this.diffPrefs,
+ context: 0,
+ line_length: Math.min(this.diffPrefs.line_length, 100),
+ line_wrapping: true,
+ };
+ }
}
declare global {
interface HTMLElementTagNameMap {
- 'gr-user-suggestion-fix': GrUserSuggetionFix;
+ 'gr-user-suggestion-fix': GrUserSuggestionsFix;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
index 50bfebf..423fe62 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
@@ -6,18 +6,28 @@
import '../../../test/common-test-setup';
import './gr-user-suggestion-fix';
import {fixture, html, assert} from '@open-wc/testing';
-import {GrUserSuggetionFix} from './gr-user-suggestion-fix';
-import {getAppContext} from '../../../services/app-context';
+import {GrUserSuggestionsFix} from './gr-user-suggestion-fix';
+import {
+ CommentModel,
+ commentModelToken,
+} from '../gr-comment-model/gr-comment-model';
+import {wrapInProvider} from '../../../models/di-provider-element';
+import {createComment} from '../../../test/test-data-generators';
suite('gr-user-suggestion-fix tests', () => {
- let element: GrUserSuggetionFix;
+ let element: GrUserSuggestionsFix;
setup(async () => {
- const flagsService = getAppContext().flagsService;
- sinon.stub(flagsService, 'isEnabled').returns(true);
- element = await fixture<GrUserSuggetionFix>(html`
- <gr-user-suggestion-fix>Hello World</gr-user-suggestion-fix>
- `);
+ const commentModel = new CommentModel({comment: createComment()});
+ element = (
+ await fixture<GrUserSuggestionsFix>(
+ wrapInProvider(
+ html` <gr-user-suggestion-fix>Hello World</gr-user-suggestion-fix> `,
+ commentModelToken,
+ commentModel
+ )
+ )
+ ).querySelector<GrUserSuggestionsFix>('gr-user-suggestion-fix')!;
await element.updateComplete;
});
@@ -45,7 +55,13 @@
></gr-copy-clipboard>
</div>
<div>
- <gr-button class="action show-fix" secondary="" flatten=""
+ <gr-button
+ aria-disabled="false"
+ class="action show-fix"
+ secondary=""
+ role="button"
+ tabindex="0"
+ flatten=""
>Show edit</gr-button
>
</div>
diff --git a/polygerrit-ui/app/embed/diff-old/gr-diff/gr-diff-styles.ts b/polygerrit-ui/app/embed/diff-old/gr-diff/gr-diff-styles.ts
index e7f4b51..a6dddd6 100644
--- a/polygerrit-ui/app/embed/diff-old/gr-diff/gr-diff-styles.ts
+++ b/polygerrit-ui/app/embed/diff-old/gr-diff/gr-diff-styles.ts
@@ -658,7 +658,7 @@
gr-selection-action-box {
/* Needs z-index to appear above wrapped content, since it's inserted
into DOM before it. */
- z-index: 10;
+ z-index: 120;
}
gr-diff-image-new,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
index 52b1467..8ef2f82 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
@@ -364,7 +364,7 @@
fire(this, 'line-mouse-enter', {lineNum: lineNumber, side})}
@mouseleave=${() =>
fire(this, 'line-mouse-leave', {lineNum: lineNumber, side})}
- >${lineNumber === FILE ? 'File' : lineNumber.toString()}</button>
+ >${lineNumber === FILE ? 'FILE' : lineNumber.toString()}</button>
`;
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts
index 01661fe..a9e332a 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts
@@ -210,7 +210,12 @@
token: newHighlight,
element,
} = this.findTokenAncestor(e?.target);
- if (!newHighlight || newHighlight === this.currentHighlight) return;
+ if (
+ !newHighlight ||
+ (this.currentHighlight === newHighlight &&
+ this.currentHighlightLineNumber === line)
+ )
+ return;
this.hoveredElement = element;
this.updateTokenTask = debounce(
this.updateTokenTask,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts
index 910575c..56336f0 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-element_test.ts
@@ -122,7 +122,7 @@
id="left-button-FILE"
tabindex="-1"
>
- File
+ FILE
</button>
</td>
<td class="gr-diff lineNum right" data-value="FILE">
@@ -133,7 +133,7 @@
id="right-button-FILE"
tabindex="-1"
>
- File
+ FILE
</button>
</td>
<td
@@ -1368,7 +1368,7 @@
id="left-button-FILE"
tabindex="-1"
>
- File
+ FILE
</button>
</td>
<td class="gr-diff left no-intraline-info sign"></td>
@@ -1383,7 +1383,7 @@
id="right-button-FILE"
tabindex="-1"
>
- File
+ FILE
</button>
</td>
<td class="gr-diff no-intraline-info right sign"></td>
@@ -2965,7 +2965,7 @@
id="left-button-FILE"
tabindex="-1"
>
- File
+ FILE
</button>
</td>
<td class="gr-diff left no-intraline-info sign"></td>
@@ -2980,7 +2980,7 @@
id="right-button-FILE"
tabindex="-1"
>
- File
+ FILE
</button>
</td>
<td class="gr-diff no-intraline-info right sign"></td>
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
index e7f4b51..1b908cd 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
@@ -461,6 +461,9 @@
color: var(--link-color);
padding: var(--spacing-m) 0 var(--spacing-m) 48px;
}
+ /* for new diff */
+ gr-diff-element,
+ /* for old diff, TODO: remove */
#diffTable {
/* for gr-selection-action-box positioning */
position: relative;
@@ -658,7 +661,7 @@
gr-selection-action-box {
/* Needs z-index to appear above wrapped content, since it's inserted
into DOM before it. */
- z-index: 10;
+ z-index: 120;
}
gr-diff-image-new,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
index c41dc91..7e30581 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
@@ -226,7 +226,7 @@
return FULL_CONTEXT;
}
if (
- prefsContext &&
+ prefsContext !== undefined &&
!(showFullContext === FullContext.NO && prefsContext === FULL_CONTEXT)
) {
return prefsContext;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
index 44f4f60..f425e2b 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
@@ -196,6 +196,12 @@
assert.equal(computeContext(1, FullContext.UNDECIDED, 2), 1);
});
+ test('computeContext 0', () => {
+ assert.equal(computeContext(0, FullContext.YES, 2), FULL_CONTEXT);
+ assert.equal(computeContext(0, FullContext.NO, 2), 0);
+ assert.equal(computeContext(0, FullContext.UNDECIDED, 2), 0);
+ });
+
test('computeContext FULL_CONTEXT', () => {
assert.equal(
computeContext(FULL_CONTEXT, FullContext.YES, 2),
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index ec6ed03..5d23da1 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -162,6 +162,7 @@
@property({type: Boolean})
lineWrapping = false;
+ // TODO: Migrate users to using the same property in render preferences.
@property({type: String})
viewMode = DiffViewMode.SIDE_BY_SIDE;
@@ -212,12 +213,15 @@
@property({type: Array})
blame: BlameInfo[] | null = null;
+ // TODO: Migrate users to using the same property in render preferences.
@property({type: Boolean})
showNewlineWarningLeft = false;
+ // TODO: Migrate users to using the same property in render preferences.
@property({type: Boolean})
showNewlineWarningRight = false;
+ // TODO: Migrate users to using the same property in render preferences.
@property({type: Boolean})
useNewImageDiffUi = false;
@@ -330,14 +334,31 @@
changedProperties.has('path') ||
changedProperties.has('renderPrefs') ||
changedProperties.has('viewMode') ||
+ changedProperties.has('loggedIn') ||
+ changedProperties.has('useNewImageDiffUi') ||
+ changedProperties.has('showNewlineWarningLeft') ||
+ changedProperties.has('showNewlineWarningRight') ||
changedProperties.has('prefs') ||
changedProperties.has('lineOfInterest')
) {
if (this.diff && this.prefs) {
const renderPrefs = {...(this.renderPrefs ?? {})};
+ // TODO: Migrate users to using render preferences directly. Then removes these overrides.
if (renderPrefs.view_mode === undefined) {
renderPrefs.view_mode = this.viewMode;
}
+ if (renderPrefs.can_comment === undefined) {
+ renderPrefs.can_comment = this.loggedIn;
+ }
+ if (renderPrefs.use_new_image_diff_ui === undefined) {
+ renderPrefs.use_new_image_diff_ui = this.useNewImageDiffUi;
+ }
+ if (renderPrefs.show_newline_warning_left === undefined) {
+ renderPrefs.show_newline_warning_left = this.showNewlineWarningLeft;
+ }
+ if (renderPrefs.show_newline_warning_right === undefined) {
+ renderPrefs.show_newline_warning_right = this.showNewlineWarningRight;
+ }
this.diffModel.updateState({
diff: this.diff,
path: this.path,
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
index b13a16f..e82c262 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -22,6 +22,7 @@
ReviewerInput,
AttentionSetInput,
RelatedChangeAndCommitInfo,
+ ReviewResult,
} from '../../types/common';
import {getUserId} from '../../utils/account-util';
import {getChangeNumber} from '../../utils/change-util';
@@ -164,7 +165,7 @@
addReviewers(
changedReviewers: Map<ReviewerState, (AccountInfo | GroupInfo)[]>,
reason: string
- ): Promise<Response>[] {
+ ): Promise<ReviewResult | undefined>[] {
const current = this.getState();
const changes = current.selectedChangeNums.map(
changeNum => current.allChanges.get(changeNum)!
@@ -177,7 +178,7 @@
this.getNewReviewersToChange(change, state, changedReviewers)
);
if (reviewersNewToChange.length === 0) {
- return Promise.resolve(new Response());
+ return Promise.resolve(undefined);
}
const attentionSetUpdates: AttentionSetInput[] = reviewersNewToChange
.filter(reviewerInput => reviewerInput.state === ReviewerState.REVIEWER)
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
index e2f75f7..ece126c 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
@@ -261,9 +261,7 @@
let saveChangeReviewStub: sinon.SinonStub;
setup(async () => {
- saveChangeReviewStub = stubRestApi('saveChangeReview').resolves(
- new Response()
- );
+ saveChangeReviewStub = stubRestApi('saveChangeReview').resolves({});
stubRestApi('getDetailedChangesWithActions').resolves([
{...changes[0], actions: {abandon: {method: HttpMethod.POST}}},
{...changes[1], status: ChangeStatus.ABANDONED},
diff --git a/polygerrit-ui/app/models/change/related-changes-model_test.ts b/polygerrit-ui/app/models/change/related-changes-model_test.ts
index 295f284..0bf7a76 100644
--- a/polygerrit-ui/app/models/change/related-changes-model_test.ts
+++ b/polygerrit-ui/app/models/change/related-changes-model_test.ts
@@ -270,7 +270,8 @@
messages: [
{
...createChangeMessage(),
- message: 'Created a revert of this change as 123',
+ message:
+ 'Created a revert of this change as If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
tag: MessageTag.TAG_REVERT as ReviewInputTag,
},
],
diff --git a/polygerrit-ui/app/models/checks/checks-fakes.ts b/polygerrit-ui/app/models/checks/checks-fakes.ts
index d50ddba..1890639 100644
--- a/polygerrit-ui/app/models/checks/checks-fakes.ts
+++ b/polygerrit-ui/app/models/checks/checks-fakes.ts
@@ -26,6 +26,7 @@
isSingleAttempt: true,
isLatestAttempt: true,
attemptDetails: [],
+ worstCategory: Category.ERROR,
results: [
{
internalResultId: 'f0r0',
@@ -94,6 +95,7 @@
isSingleAttempt: true,
isLatestAttempt: true,
attemptDetails: [],
+ worstCategory: Category.ERROR,
results: [
{
internalResultId: 'f1r0',
@@ -228,6 +230,7 @@
callback: () => Promise.resolve({message: 'fake "delete" triggered'}),
},
],
+ worstCategory: Category.INFO,
results: [
{
internalResultId: 'f2r0',
@@ -276,6 +279,7 @@
isSingleAttempt: false,
isLatestAttempt: false,
attemptDetails: [],
+ worstCategory: Category.INFO,
results: [
{
internalResultId: 'f42r0',
@@ -294,6 +298,7 @@
isSingleAttempt: false,
isLatestAttempt: false,
attemptDetails: [],
+ worstCategory: Category.ERROR,
results: [
{
internalResultId: 'f43r0',
@@ -320,6 +325,7 @@
isSingleAttempt: false,
isLatestAttempt: true,
attemptDetails: [],
+ worstCategory: Category.INFO,
results: [
{
internalResultId: 'f44r0',
@@ -380,6 +386,7 @@
isSingleAttempt: false,
isLatestAttempt: false,
attemptDetails: [],
+ worstCategory: Category.ERROR,
results:
attempt % 2 === 0
? [
diff --git a/polygerrit-ui/app/models/checks/checks-model.ts b/polygerrit-ui/app/models/checks/checks-model.ts
index 25785e4..612862b 100644
--- a/polygerrit-ui/app/models/checks/checks-model.ts
+++ b/polygerrit-ui/app/models/checks/checks-model.ts
@@ -9,6 +9,7 @@
createAttemptMap,
LATEST_ATTEMPT,
sortAttemptDetails,
+ worstCategory,
} from './checks-util';
import {assertIsDefined} from '../../utils/common-util';
import {select} from '../../utils/observable-util';
@@ -104,6 +105,12 @@
* List of all attempts for the same check, ordered by attempt number.
*/
attemptDetails: AttemptDetail[];
+
+ /**
+ * The category of the worst check result in the run.
+ */
+ worstCategory?: Category;
+
results?: CheckResult[];
}
@@ -119,7 +126,18 @@
Pick<CheckRun, 'patchset'> &
Pick<CheckRun, 'isLatestAttempt'> &
Pick<CheckRun, 'checkName'> &
- Pick<CheckRun, 'labelName'> & {results?: never};
+ Pick<CheckRun, 'labelName'> &
+ Pick<CheckRun, 'status'> &
+ Pick<CheckRun, 'statusLink'> &
+ Pick<CheckRun, 'statusDescription'> &
+ Pick<CheckRun, 'startedTimestamp'> &
+ Pick<CheckRun, 'scheduledTimestamp'> &
+ Pick<CheckRun, 'finishedTimestamp'> &
+ Pick<CheckRun, 'checkLink'> &
+ Pick<CheckRun, 'checkDescription'> &
+ Pick<CheckRun, 'actions'> &
+ Pick<CheckRun, 'attemptDetails'> &
+ Pick<CheckRun, 'worstCategory'> & {results?: never};
export function runResult(run: CheckRun, result: CheckResult): RunResult {
return {
@@ -129,6 +147,17 @@
isLatestAttempt: run.isLatestAttempt,
checkName: run.checkName,
labelName: run.labelName,
+ status: run.status,
+ statusLink: run.statusLink,
+ statusDescription: run.statusDescription,
+ startedTimestamp: run.startedTimestamp,
+ scheduledTimestamp: run.scheduledTimestamp,
+ finishedTimestamp: run.finishedTimestamp,
+ checkLink: run.checkLink,
+ checkDescription: run.checkDescription,
+ actions: run.actions,
+ attemptDetails: run.attemptDetails,
+ worstCategory: run.worstCategory,
...result,
};
}
@@ -593,6 +622,7 @@
isLatestAttempt: attemptInfo.latestAttempt === (run.attempt ?? 0),
isSingleAttempt: attemptInfo.isSingleAttempt,
attemptDetails: attemptInfo.attempts,
+ worstCategory: worstCategory(run),
results: (run.results ?? []).map((result, i) => {
return {
...result,
@@ -637,7 +667,11 @@
}
return result;
});
- return resultUpdated ? {...run, results} : run;
+ if (resultUpdated) {
+ run = {...run, results};
+ run.worstCategory = worstCategory(run);
+ }
+ return run;
});
if (!runUpdated) return;
pluginState[pluginName] = {
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index 86c4b49..a567fb5 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -164,7 +164,7 @@
return r;
}
-export function worstCategory(run: CheckRun) {
+export function worstCategory(run: CheckRunApi) {
if (hasResultsOf(run, Category.ERROR)) return Category.ERROR;
if (hasResultsOf(run, Category.WARNING)) return Category.WARNING;
if (hasResultsOf(run, Category.INFO)) return Category.INFO;
@@ -288,7 +288,7 @@
)[0];
}
-export function runActions(run?: CheckRun): Action[] {
+export function runActions(run?: CheckRun | RunResult): Action[] {
if (!run?.actions) return [];
return run.actions.map(action => toCanonicalAction(action, run.status));
}
@@ -297,7 +297,7 @@
if (run.status !== RunStatus.COMPLETED) {
return iconFor(run.status);
} else {
- const category = worstCategory(run);
+ const category = run.worstCategory;
return category ? iconFor(category) : iconFor(run.status);
}
}
@@ -340,16 +340,16 @@
);
}
-export function hasResultsOf(run: CheckRun, category: Category) {
+export function hasResultsOf(run: CheckRunApi, category: Category) {
return getResultsOf(run, category).length > 0;
}
-export function getResultsOf(run: CheckRun, category: Category) {
+export function getResultsOf(run: CheckRunApi, category: Category) {
return (run.results ?? []).filter(r => r.category === category);
}
export function compareByWorstCategory(a: CheckRun, b: CheckRun) {
- const catComp = catLevel(worstCategory(b)) - catLevel(worstCategory(a));
+ const catComp = catLevel(b.worstCategory) - catLevel(a.worstCategory);
if (catComp !== 0) return catComp;
const statusComp = runLevel(b.status) - runLevel(a.status);
return statusComp;
@@ -490,6 +490,7 @@
isSingleAttempt: false,
isLatestAttempt: false,
attemptDetails: [],
+ worstCategory: worstCategory(run),
results: (run.results ?? []).map(fromApiToInternalResult),
};
}
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index c9b0bd9..7bf2785 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -23,6 +23,7 @@
} from '../../types/common';
import {
addPath,
+ convertToCommentInput,
createNew,
createNewPatchsetLevel,
id,
@@ -438,7 +439,6 @@
private readonly navigation: NavigationService
) {
super(initialState);
- console.info('CommentsModel constrcutor');
this.subscriptions.push(
this.savingInProgress$.subscribe(savingInProgress => {
if (savingInProgress) {
@@ -478,7 +478,6 @@
);
this.subscriptions.push(
this.changeViewModel.changeNum$.subscribe(changeNum => {
- console.info(`CommentsModel reload ${changeNum}`);
this.changeNum = changeNum;
this.setState({...initialState});
this.reloadAllComments();
@@ -520,18 +519,8 @@
this.setState(reducer({...this.getState()}));
}
- override setState(state: CommentState) {
- const commentsUndefPrev = this.getState().comments === undefined;
- const commentsUndefNext = state.comments === undefined;
- console.info(
- `CommentsModel setState ${commentsUndefPrev} ${commentsUndefNext} ${this.stateUpdateInProgress}`
- );
- super.setState(state);
- }
-
async reloadComments(changeNum: NumericChangeId): Promise<void> {
const comments = await this.restApiService.getDiffComments(changeNum);
- console.info(`CommentsModel setComments ${comments === undefined}`);
this.modifyState(s => setComments(s, comments));
}
@@ -650,7 +639,7 @@
const result = await this.restApiService.saveDiffDraft(
changeNum,
draft.patch_set,
- draft
+ convertToCommentInput(draft)
);
if (changeNum !== this.changeNum) return draft;
if (!result.ok) throw new Error('request failed');
diff --git a/polygerrit-ui/app/models/model.ts b/polygerrit-ui/app/models/model.ts
index 2d0ff42..19b52fc 100644
--- a/polygerrit-ui/app/models/model.ts
+++ b/polygerrit-ui/app/models/model.ts
@@ -27,7 +27,7 @@
* another `next()` call. So make sure that state updates complete before
* starting another one.
*/
- protected stateUpdateInProgress = false;
+ private stateUpdateInProgress = false;
private subject$: BehaviorSubject<T>;
diff --git a/polygerrit-ui/app/models/plugins/plugins-model.ts b/polygerrit-ui/app/models/plugins/plugins-model.ts
index 83235b17..1d38c17 100644
--- a/polygerrit-ui/app/models/plugins/plugins-model.ts
+++ b/polygerrit-ui/app/models/plugins/plugins-model.ts
@@ -12,7 +12,8 @@
} from '../../api/checks';
import {Model} from '../model';
import {select} from '../../utils/observable-util';
-import {CoverageProvider} from '../../api/annotation';
+import {CoverageProvider, TokenHoverListener} from '../../api/annotation';
+import {SuggestionsProvider} from '../../api/suggestions';
export interface CoveragePlugin {
pluginName: string;
@@ -25,6 +26,16 @@
config: ChecksApiConfig;
}
+export interface SuggestionPlugin {
+ pluginName: string;
+ provider: SuggestionsProvider;
+}
+
+export interface TokenHoverListenerPlugin {
+ pluginName: string;
+ listener: TokenHoverListener;
+}
+
export interface ChecksUpdate {
pluginName: string;
run: CheckRun;
@@ -41,6 +52,17 @@
* List of plugins that have called checks().register().
*/
checksPlugins: ChecksPlugin[];
+
+ /**
+ * List of plugins that have called suggestions().register().
+ */
+ suggestionsPlugins: SuggestionPlugin[];
+
+ /**
+ * List of plugins that have called
+ * annotationApi().addTokenHoverListener().
+ */
+ tokenHighlightPlugins: TokenHoverListenerPlugin[];
}
export class PluginsModel extends Model<PluginsState> {
@@ -66,6 +88,8 @@
super({
coveragePlugins: [],
checksPlugins: [],
+ suggestionsPlugins: [],
+ tokenHighlightPlugins: [],
});
}
@@ -101,6 +125,38 @@
this.setState(nextState);
}
+ suggestionsRegister(plugin: SuggestionPlugin) {
+ const nextState = {...this.getState()};
+ nextState.suggestionsPlugins = [...nextState.suggestionsPlugins];
+ const alreadyRegistered = nextState.suggestionsPlugins.some(
+ p => p.pluginName === plugin.pluginName
+ );
+ if (alreadyRegistered) {
+ console.warn(
+ `${plugin.pluginName} tried to register twice as a suggestion provider. Ignored.`
+ );
+ return;
+ }
+ nextState.suggestionsPlugins.push(plugin);
+ this.setState(nextState);
+ }
+
+ tokenHoverListenerRegister(plugin: TokenHoverListenerPlugin) {
+ const nextState = {...this.getState()};
+ nextState.tokenHighlightPlugins = [...nextState.tokenHighlightPlugins];
+ const alreadyRegistered = nextState.tokenHighlightPlugins.some(
+ p => p.pluginName === plugin.pluginName
+ );
+ if (alreadyRegistered) {
+ console.warn(
+ `${plugin.pluginName} tried to register twice as a hover callback. Ignored.`
+ );
+ return;
+ }
+ nextState.tokenHighlightPlugins.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/views/documentation.ts b/polygerrit-ui/app/models/views/documentation.ts
index 6789695..abb0f03 100644
--- a/polygerrit-ui/app/models/views/documentation.ts
+++ b/polygerrit-ui/app/models/views/documentation.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {GerritView} from '../../services/router/router-model';
+import {getBaseUrl} from '../../utils/url-util';
import {define} from '../dependency';
import {Model} from '../model';
import {ViewState} from './base';
@@ -13,6 +14,10 @@
filter: string;
}
+export function createDocumentationUrl() {
+ return `${getBaseUrl()}/Documentation`;
+}
+
export const documentationViewModelToken = define<DocumentationViewModel>(
'documentation-view-model'
);
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index e55c6d3..a8d4a3b 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -18,4 +18,6 @@
CHECKS_DEVELOPER = 'UiFeature__checks_developer',
PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
+ ML_SUGGESTED_EDIT = 'UiFeature__ml_suggested_edit',
+ DIFF_FOR_USER_SUGGESTED_EDIT = 'UiFeature__diff_for_user_suggested_edit',
}
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 06f6321..fc4123b 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
@@ -22,10 +22,7 @@
import {getBaseUrl} from '../../utils/url-util';
import {Finalizable} from '../registry';
import {getParentIndex, isMergeParent} from '../../utils/patch-set-util';
-import {
- ListChangesOption,
- listChangesOptionsToHex,
-} from '../../utils/change-util';
+import {listChangesOptionsToHex} from '../../utils/change-util';
import {assertNever, hasOwnProperty} from '../../utils/common-util';
import {AuthService} from '../gr-auth/gr-auth';
import {
@@ -119,6 +116,8 @@
UrlEncodedCommentId,
FixReplacementInfo,
DraftInfo,
+ ListChangesOption,
+ ReviewResult,
} from '../../types/common';
import {
DiffInfo,
@@ -311,7 +310,9 @@
finalize() {}
- _fetchSharedCacheURL(req: FetchJSONRequest): Promise<ParsedJSON | undefined> {
+ _fetchSharedCacheURL(
+ req: FetchJSONRequest
+ ): Promise<AccountDetailInfo | ParsedJSON | undefined> {
// Cache is shared across instances
return this._restApiHelper.fetchCacheURL(req);
}
@@ -759,6 +760,14 @@
}) as Promise<AccountExternalIdInfo[] | undefined>;
}
+ deleteAccount() {
+ return this._restApiHelper.send({
+ method: HttpMethod.DELETE,
+ url: '/accounts/self',
+ reportUrlAsIs: true,
+ });
+ }
+
deleteAccountIdentity(id: string[]) {
return this._restApiHelper.send({
method: HttpMethod.POST,
@@ -1019,7 +1028,7 @@
/**
* Construct the uri to get list of changes.
*
- * If options is undefined then default options (see _getChangesOptionsHex) is
+ * If options is undefined then default options (see getListChangesOptionsHex) is
* used.
*/
getRequestForGetChanges(
@@ -1028,7 +1037,7 @@
offset?: 'n,z' | number,
options?: string
) {
- options = options || this._getChangesOptionsHex();
+ options = options || this.getListChangesOptionsHex();
if (offset === 'n,z') {
offset = 0;
}
@@ -1053,7 +1062,7 @@
/**
* For every query fetches the matching changes.
*
- * If options is undefined then default options (see _getChangesOptionsHex) is
+ * If options is undefined then default options (see getListChangesOptionsHex) is
* used.
*/
getChangesForMultipleQueries(
@@ -1100,7 +1109,7 @@
/**
* Fetches changes that match the query.
*
- * If options is undefined then default options (see _getChangesOptionsHex) is
+ * If options is undefined then default options (see getListChangesOptionsHex) is
* used.
*/
getChanges(
@@ -1147,7 +1156,6 @@
undefined,
listChangesOptionsToHex(
ListChangesOption.CHANGE_ACTIONS,
- ListChangesOption.CURRENT_ACTIONS,
ListChangesOption.CURRENT_REVISION,
ListChangesOption.DETAILED_LABELS,
// TODO: remove this option and merge requirements from dashboard req
@@ -1176,34 +1184,33 @@
);
}
- getChangeDetail(
+ async getChangeDetail(
changeNum?: NumericChangeId,
errFn?: ErrorCallback,
cancelCondition?: CancelConditionCallback
): Promise<ParsedChangeInfo | undefined> {
- if (!changeNum) return Promise.resolve(undefined);
- return this.getConfig(false).then(config => {
- const optionsHex = this._getChangeOptionsHex(config);
- return this._getChangeDetail(
- changeNum,
- optionsHex,
- errFn,
- cancelCondition
- ).then(detail =>
- // detail has ChangeViewChangeInfo type because the optionsHex always
- // includes ALL_REVISIONS flag.
- GrReviewerUpdatesParser.parse(detail as ChangeViewChangeInfo)
- );
- });
+ if (!changeNum) return;
+ const optionsHex = listChangesOptionsToHex(
+ ...(await this.getChangeOptions())
+ );
+
+ return this._getChangeDetail(
+ changeNum,
+ optionsHex,
+ errFn,
+ cancelCondition
+ ).then(detail =>
+ // detail has ChangeViewChangeInfo type because the optionsHex always
+ // includes ALL_REVISIONS flag.
+ GrReviewerUpdatesParser.parse(detail as ChangeViewChangeInfo)
+ );
}
- _getChangesOptionsHex() {
- if (
- window.DEFAULT_DETAIL_HEXES &&
- window.DEFAULT_DETAIL_HEXES.dashboardPage
- ) {
- return window.DEFAULT_DETAIL_HEXES.dashboardPage;
- }
+ /**
+ * Returns the options to use for querying multiple changes (e.g. dashboard or search).
+ * @return The options hex to use when fetching multiple changes.
+ */
+ private getListChangesOptionsHex() {
const options = [
ListChangesOption.LABELS,
ListChangesOption.DETAILED_ACCOUNTS,
@@ -1214,21 +1221,17 @@
return listChangesOptionsToHex(...options);
}
- _getChangeOptionsHex(config?: ServerInfo) {
- if (
- window.DEFAULT_DETAIL_HEXES &&
- window.DEFAULT_DETAIL_HEXES.changePage &&
- (!config || !(config.receive && config.receive.enable_signed_push))
- ) {
- return window.DEFAULT_DETAIL_HEXES.changePage;
- }
+ async getChangeOptions(): Promise<number[]> {
+ const config = await this.getConfig(false);
// This list MUST be kept in sync with
// ChangeIT#changeDetailsDoesNotRequireIndex
+ // This list MUST be kept in sync with getResponseFormatOptions
const options = [
ListChangesOption.ALL_COMMITS,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CHANGE_ACTIONS,
+ ListChangesOption.DETAILED_ACCOUNTS,
ListChangesOption.DETAILED_LABELS,
ListChangesOption.DOWNLOAD_COMMANDS,
ListChangesOption.MESSAGES,
@@ -1240,7 +1243,32 @@
if (config?.receive?.enable_signed_push) {
options.push(ListChangesOption.PUSH_CERTIFICATES);
}
- return listChangesOptionsToHex(...options);
+ return options;
+ }
+
+ async getResponseFormatOptions(): Promise<string[]> {
+ const config = await this.getConfig(false);
+
+ // This list MUST be kept in sync with
+ // ChangeIT#changeDetailsDoesNotRequireIndex
+ // This list MUST be kept in sync with getChangeOptions
+ const options = [
+ 'ALL_COMMITS',
+ 'ALL_REVISIONS',
+ 'CHANGE_ACTIONS',
+ 'DETAILED_LABELS',
+ 'DETAILED_ACCOUNTS',
+ 'DOWNLOAD_COMMANDS',
+ 'MESSAGES',
+ 'SUBMITTABLE',
+ 'WEB_LINKS',
+ 'SKIP_DIFFSTAT',
+ 'SUBMIT_REQUIREMENTS',
+ ];
+ if (config?.receive?.enable_signed_push) {
+ options.push('PUSH_CERTIFICATES');
+ }
+ return options;
}
/**
@@ -1949,37 +1977,36 @@
});
}
- saveChangeReview(
- changeNum: NumericChangeId,
- patchNum: PatchSetNum,
- review: ReviewInput
- ): Promise<Response>;
-
- saveChangeReview(
+ async saveChangeReview(
changeNum: NumericChangeId,
patchNum: PatchSetNum,
review: ReviewInput,
- errFn: ErrorCallback
- ): Promise<Response | undefined>;
-
- saveChangeReview(
- changeNum: NumericChangeId,
- patchNum: PatchSetNum,
- review: ReviewInput,
- errFn?: ErrorCallback
+ errFn?: ErrorCallback,
+ fetchDetail?: boolean
) {
+ if (fetchDetail) {
+ review.response_format_options = await this.getResponseFormatOptions();
+ }
const promises: [Promise<void>, Promise<string>] = [
this.awaitPendingDiffDrafts(),
this.getChangeActionURL(changeNum, patchNum, '/review'),
];
- return Promise.all(promises).then(([, url]) =>
- this._restApiHelper.send({
- method: HttpMethod.POST,
- url,
- body: review,
- errFn,
- })
- );
+ return Promise.all(promises)
+ .then(([, url]) =>
+ this._restApiHelper.send({
+ method: HttpMethod.POST,
+ url,
+ body: review,
+ errFn,
+ parseResponse: true,
+ })
+ )
+ .then(payload => {
+ if (!payload) {
+ return undefined;
+ }
+ return payload as unknown as ReviewResult;
+ });
}
getChangeEdit(changeNum?: NumericChangeId): Promise<EditInfo | undefined> {
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
index 764aa98..8b7e3bd 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl_test.ts
@@ -12,10 +12,7 @@
waitEventLoop,
} from '../../test/test-utils';
import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
-import {
- ListChangesOption,
- listChangesOptionsToHex,
-} from '../../utils/change-util';
+import {listChangesOptionsToHex} from '../../utils/change-util';
import {
createAccountDetailWithId,
createChange,
@@ -47,6 +44,7 @@
EditPreferencesInfo,
Hashtag,
HashtagsInput,
+ ListChangesOption,
NumericChangeId,
PARENT,
ParsedJSON,
@@ -67,7 +65,9 @@
const EXPECTED_QUERY_OPTIONS = listChangesOptionsToHex(
ListChangesOption.CHANGE_ACTIONS,
- ListChangesOption.CURRENT_ACTIONS,
+ // Current actions can be costly to calculate (e.g submit action)
+ // They are not used in bulk actions.
+ // ListChangesOption.CURRENT_ACTIONS,
ListChangesOption.CURRENT_REVISION,
ListChangesOption.DETAILED_LABELS,
ListChangesOption.SUBMIT_REQUIREMENTS
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index d8bf276..1bf5c90 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -91,6 +91,7 @@
UrlEncodedCommentId,
UserId,
DraftInfo,
+ ReviewResult,
} from '../../types/common';
import {
DiffInfo,
@@ -123,6 +124,7 @@
): Promise<AccountCapabilityInfo | undefined>;
getExternalIds(): Promise<AccountExternalIdInfo[] | undefined>;
deleteAccountIdentity(id: string[]): Promise<unknown>;
+ deleteAccount(): Promise<unknown>;
getRepos(
filter: string | undefined,
reposPerPage: number,
@@ -351,20 +353,10 @@
saveChangeReview(
changeNum: ChangeId | NumericChangeId,
patchNum: RevisionId,
- review: ReviewInput
- ): Promise<Response>;
- saveChangeReview(
- changeNum: ChangeId | NumericChangeId,
- patchNum: RevisionId,
review: ReviewInput,
- errFn: ErrorCallback
- ): Promise<Response | undefined>;
- saveChangeReview(
- changeNum: ChangeId | NumericChangeId,
- patchNum: RevisionId,
- review: ReviewInput,
- errFn?: ErrorCallback
- ): Promise<Response>;
+ errFn?: ErrorCallback,
+ fetch_detail?: boolean
+ ): Promise<ReviewResult | undefined>;
getChangeEdit(changeNum?: NumericChangeId): Promise<EditInfo | undefined>;
diff --git a/polygerrit-ui/app/services/service-worker-installer_test.ts b/polygerrit-ui/app/services/service-worker-installer_test.ts
index a036289..e13cf19 100644
--- a/polygerrit-ui/app/services/service-worker-installer_test.ts
+++ b/polygerrit-ui/app/services/service-worker-installer_test.ts
@@ -20,19 +20,31 @@
const userModel = testResolver(userModelToken);
sinon.stub(flagsService, 'isEnabled').returns(true);
new ServiceWorkerInstaller(flagsService, reportingService, userModel);
+ // TODO: There is a race-condition betweeen preferences being set here
+ // and being loaded from the rest-api-service when the user-model gets created.
+ // So we explicitly wait for the allow_browser_notifications to be false
+ // before continuing with the test.
+ // Ideally there's a way to wait for models to stabilize.
const prefs = {
...createDefaultPreferences(),
+ allow_browser_notifications: false,
+ };
+ await waitUntilObserved(
+ userModel.preferences$,
+ pref => pref.allow_browser_notifications === false
+ );
+ userModel.setPreferences(prefs);
+
+ const prefs2 = {
+ ...createDefaultPreferences(),
allow_browser_notifications: true,
};
- userModel.setPreferences(prefs);
+ userModel.setPreferences(prefs2);
await waitUntilObserved(
userModel.preferences$,
pref => pref.allow_browser_notifications === true
);
- await waitUntilObserved(
- userModel.preferences$,
- pref => pref.allow_browser_notifications === true
- );
+
assert.isTrue(registerStub.called);
});
});
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index bfa881e..531697d 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -68,13 +68,13 @@
createCommit,
createConfig,
createMergeable,
- createPreferences,
createServerInfo,
createSubmittedTogetherInfo,
} from '../test-data-generators';
import {
createDefaultDiffPrefs,
createDefaultEditPrefs,
+ createDefaultPreferences,
} from '../../constants/constants';
import {ParsedChangeInfo} from '../../types/types';
import {getBaseUrl} from '../../utils/url-util';
@@ -123,6 +123,9 @@
createRepoTag(): Promise<Response> {
return Promise.resolve(new Response());
},
+ deleteAccount(): Promise<Response> {
+ return Promise.resolve(new Response());
+ },
deleteAccountEmail(): Promise<Response> {
return Promise.resolve(new Response());
},
@@ -378,8 +381,7 @@
return Promise.resolve({});
},
getPreferences(): Promise<PreferencesInfo | undefined> {
- // TODO: Use createDefaultPreferences() instead.
- return Promise.resolve(createPreferences());
+ return Promise.resolve(createDefaultPreferences());
},
getProjectConfig(): Promise<ConfigInfo | undefined> {
return Promise.resolve(createConfig());
@@ -479,7 +481,7 @@
return Promise.resolve(new Response());
},
saveChangeReview() {
- return Promise.resolve(new Response());
+ return Promise.resolve({});
},
saveChangeStarred(): Promise<Response> {
return Promise.resolve(new Response());
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 205bda4..38d50d5 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -1129,6 +1129,8 @@
pluginName: 'test-plugin-name',
summary: 'This is the test summary.',
message: 'This is the test message.',
+ status: RunStatus.COMPLETED,
+ attemptDetails: [{attempt: 'latest'}],
};
}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 766b407..b23fc7e 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -9,7 +9,6 @@
SubmitType,
InheritedBooleanInfoConfiguredValue,
PermissionAction,
- CommentSide,
AppTheme,
DateFormat,
TimeFormat,
@@ -42,8 +41,10 @@
ChangeMessageId,
ChangeMessageInfo,
ChangeSubmissionId,
+ CommentInfo,
CommentLinkInfo,
CommentLinks,
+ CommentSide,
CommitId,
CommitInfo,
ConfigArrayParameterInfo,
@@ -51,6 +52,7 @@
ConfigListParameterInfo,
ConfigParameterInfo,
ConfigParameterInfoBase,
+ ContextLine,
ContributorAgreementInfo,
CustomKey,
CustomKeyedValues,
@@ -108,6 +110,7 @@
SuggestInfo,
Timestamp,
TopicName,
+ UrlEncodedCommentId,
UrlEncodedRepoName,
UserConfigInfo,
VotingRangeInfo,
@@ -118,7 +121,7 @@
CommentRange,
} from '../api/rest-api';
import {DiffInfo, IgnoreWhitespaceType} from './diff';
-import {LineNumber} from '../api/diff';
+import {PatchRange, LineNumber} from '../api/diff';
export type {
AccountId,
@@ -141,6 +144,7 @@
ChangeMessageId,
ChangeMessageInfo,
ChangeSubmissionId,
+ CommentInfo,
CommentLinkInfo,
CommentLinks,
CommentRange,
@@ -151,6 +155,7 @@
ConfigListParameterInfo,
ConfigParameterInfo,
ConfigParameterInfoBase,
+ ContextLine,
ContributorAgreementInfo,
DetailedLabelInfo,
DownloadInfo,
@@ -179,6 +184,7 @@
MaxObjectSizeLimitInfo,
NumericChangeId,
ParentCommitInfo,
+ PatchRange,
PatchSetNum,
PatchSetNumber,
PluginConfigInfo,
@@ -202,6 +208,7 @@
SuggestInfo,
Timestamp,
TopicName,
+ UrlEncodedCommentId,
UrlEncodedRepoName,
UserConfigInfo,
VotingRangeInfo,
@@ -233,9 +240,6 @@
// The UUID of the suggested fix.
export type FixId = BrandType<string, '_fixId'>;
-// The URL encoded UUID of the comment
-export type UrlEncodedCommentId = BrandType<string, '_urlEncodedCommentId'>;
-
// The ID of the dashboard, in the form of '<ref>:<path>'
export type DashboardId = BrandType<string, '_dahsboardId'>;
@@ -249,6 +253,81 @@
export type UserId = AccountId | GroupId | EmailAddress;
+// Must be kept in sync with the ListChangesOption enum.
+// See: java/com/google/gerrit/extensions/client/ListChangesOption.java
+export const ListChangesOption = {
+ LABELS: 0,
+ DETAILED_LABELS: 8,
+
+ // Return information on the current patch set of the change.
+ CURRENT_REVISION: 1,
+ ALL_REVISIONS: 2,
+
+ // If revisions are included, parse the commit object.
+ CURRENT_COMMIT: 3,
+ ALL_COMMITS: 4,
+
+ // If a patch set is included, include the files of the patch set.
+ CURRENT_FILES: 5,
+ ALL_FILES: 6,
+
+ // If accounts are included, include detailed account info.
+ DETAILED_ACCOUNTS: 7,
+
+ // Include messages associated with the change.
+ MESSAGES: 9,
+
+ // Include allowed actions client could perform.
+ CURRENT_ACTIONS: 10,
+
+ // Set the reviewed boolean for the caller.
+ REVIEWED: 11,
+
+ // Include download commands for the caller.
+ DOWNLOAD_COMMANDS: 13,
+
+ // Include patch set weblinks.
+ WEB_LINKS: 14,
+
+ // Include consistency check results.
+ CHECK: 15,
+
+ // Include allowed change actions client could perform.
+ CHANGE_ACTIONS: 16,
+
+ // Include a copy of commit messages including review footers.
+ COMMIT_FOOTERS: 17,
+
+ // Include push certificate information along with any patch sets.
+ PUSH_CERTIFICATES: 18,
+
+ // Include change's reviewer updates.
+ REVIEWER_UPDATES: 19,
+
+ // Set the submittable boolean.
+ SUBMITTABLE: 20,
+
+ // If tracking ids are included, include detailed tracking ids info.
+ TRACKING_IDS: 21,
+
+ // Skip mergeability data.
+ SKIP_MERGEABLE: 22,
+
+ // Skip diffstat computation that compute the insertions field (number of lines inserted) and
+ // deletions field (number of lines deleted)
+ SKIP_DIFFSTAT: 23,
+
+ // Include the evaluated submit requirements for the caller.
+ SUBMIT_REQUIREMENTS: 24,
+
+ // Include custom keyed values.
+ CUSTOM_KEYED_VALUES: 25,
+
+ // Include the 'starred' field, that is if the change is starred by the
+ // current user.
+ STAR: 26,
+};
+
// https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#contributor-agreement-input
export interface ContributorAgreementInput {
name?: string;
@@ -842,40 +921,6 @@
commentThreads: CommentThread[];
}
-/**
- * The CommentInfo entity contains information about an inline comment.
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info
- */
-export interface CommentInfo {
- id: UrlEncodedCommentId;
- updated: Timestamp;
- // TODO(TS): Make this required. Every comment must have patch_set set.
- patch_set?: RevisionPatchSetNum;
- path?: string;
- side?: CommentSide;
- parent?: number;
- line?: number;
- range?: CommentRange;
- in_reply_to?: UrlEncodedCommentId;
- message?: string;
- author?: AccountInfo;
- tag?: string;
- unresolved?: boolean;
- change_message_id?: string;
- commit_id?: string;
- context_lines?: ContextLine[];
- source_content_type?: string;
-}
-
-/**
- * The ContextLine entity contains the line number and line text of a single line of the source file content..
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#context-line
- */
-export interface ContextLine {
- line_number: number;
- context_line: string;
-}
-
export type NameToProjectInfoMap = {[projectName: string]: ProjectInfo};
export type FilePathToDiffInfoMap = {[path: string]: DiffInfo};
@@ -1136,15 +1181,6 @@
}
/**
- * Defines a patch ranges. Used as input for gr-rest-api methods,
- * doesn't exist in Rest API
- */
-export interface PatchRange {
- patchNum: RevisionPatchSetNum;
- basePatchNum: BasePatchSetNum;
-}
-
-/**
* The CommentInput entity contains information for creating an inline comment
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-input
*/
@@ -1332,6 +1368,7 @@
add_to_attention_set?: AttentionSetInput[];
remove_from_attention_set?: AttentionSetInput[];
ignore_automatic_attention_set_rules?: boolean;
+ response_format_options?: string[];
}
/**
@@ -1343,6 +1380,7 @@
labels?: unknown;
reviewers?: {[key: UserId]: AddReviewerResult};
ready?: boolean;
+ change_info?: ChangeInfo;
}
/**
diff --git a/polygerrit-ui/app/types/globals.ts b/polygerrit-ui/app/types/globals.ts
index 4a709b0..7448a27 100644
--- a/polygerrit-ui/app/types/globals.ts
+++ b/polygerrit-ui/app/types/globals.ts
@@ -23,12 +23,6 @@
// it's defined because of limitations from typescript, which don't import .mjs
page?: unknown;
hljs?: HighlightJS;
-
- DEFAULT_DETAIL_HEXES?: {
- diffPage?: string;
- changePage?: string;
- dashboardPage?: string;
- };
STATIC_RESOURCE_PATH?: string;
PRELOADED_QUERIES?: {
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 40474e9..9f2374a 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -102,11 +102,6 @@
return !!x && Number.isInteger(x) && (x as number) > 0;
}
-export interface FileRange {
- basePath?: string;
- path: string;
-}
-
export interface FetchRequest {
url: string;
fetchOptions?: AuthRequestInit;
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index 932d91a3..5079abd 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -31,80 +31,6 @@
REWRITE: 'REWRITE',
};
-// Must be kept in sync with the ListChangesOption enum and protobuf.
-export const ListChangesOption = {
- LABELS: 0,
- DETAILED_LABELS: 8,
-
- // Return information on the current patch set of the change.
- CURRENT_REVISION: 1,
- ALL_REVISIONS: 2,
-
- // If revisions are included, parse the commit object.
- CURRENT_COMMIT: 3,
- ALL_COMMITS: 4,
-
- // If a patch set is included, include the files of the patch set.
- CURRENT_FILES: 5,
- ALL_FILES: 6,
-
- // If accounts are included, include detailed account info.
- DETAILED_ACCOUNTS: 7,
-
- // Include messages associated with the change.
- MESSAGES: 9,
-
- // Include allowed actions client could perform.
- CURRENT_ACTIONS: 10,
-
- // Set the reviewed boolean for the caller.
- REVIEWED: 11,
-
- // Include download commands for the caller.
- DOWNLOAD_COMMANDS: 13,
-
- // Include patch set weblinks.
- WEB_LINKS: 14,
-
- // Include consistency check results.
- CHECK: 15,
-
- // Include allowed change actions client could perform.
- CHANGE_ACTIONS: 16,
-
- // Include a copy of commit messages including review footers.
- COMMIT_FOOTERS: 17,
-
- // Include push certificate information along with any patch sets.
- PUSH_CERTIFICATES: 18,
-
- // Include change's reviewer updates.
- REVIEWER_UPDATES: 19,
-
- // Set the submittable boolean.
- SUBMITTABLE: 20,
-
- // If tracking ids are included, include detailed tracking ids info.
- TRACKING_IDS: 21,
-
- // Skip mergeability data.
- SKIP_MERGEABLE: 22,
-
- // Skip diffstat computation that compute the insertions field (number of lines inserted) and
- // deletions field (number of lines deleted)
- SKIP_DIFFSTAT: 23,
-
- // Include the evaluated submit requirements for the caller.
- SUBMIT_REQUIREMENTS: 24,
-
- // Include custom keyed values.
- CUSTOM_KEYED_VALUES: 25,
-
- // Include the 'starred' field, that is if the change is starred by the
- // current user.
- STAR: 26,
-};
-
export function listChangesOptionsToHex(...args: number[]) {
let v = 0;
for (let i = 0; i < args.length; i++) {
diff --git a/polygerrit-ui/app/utils/change-util_test.ts b/polygerrit-ui/app/utils/change-util_test.ts
index 1d209f9..6e53c16 100644
--- a/polygerrit-ui/app/utils/change-util_test.ts
+++ b/polygerrit-ui/app/utils/change-util_test.ts
@@ -16,6 +16,7 @@
AccountId,
ChangeStates,
CommitId,
+ ListChangesOption,
NumericChangeId,
PatchSetNum,
} from '../types/common';
@@ -27,7 +28,6 @@
changePath,
changeStatuses,
isRemovableReviewer,
- ListChangesOption,
listChangesOptionsToHex,
hasHumanReviewer,
} from './change-util';
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index f5649b6..7fad329 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -28,6 +28,7 @@
SavingState,
NewDraftInfo,
isNew,
+ CommentInput,
} from '../types/common';
import {CommentSide, SpecialFilePath} from '../constants/constants';
import {parseDate} from './date-util';
@@ -557,3 +558,36 @@
}
return comment;
}
+
+export function convertToCommentInput(comment: Comment): CommentInput {
+ const output: CommentInput = {
+ message: comment.message,
+ unresolved: comment.unresolved,
+ };
+
+ if (comment.id) {
+ output.id = comment.id;
+ }
+ if (comment.path) {
+ output.path = comment.path;
+ }
+ if (comment.side) {
+ output.side = comment.side;
+ }
+ if (comment.line) {
+ output.line = comment.line;
+ }
+ if (comment.range) {
+ output.range = comment.range;
+ }
+ if (comment.in_reply_to) {
+ output.in_reply_to = comment.in_reply_to;
+ }
+ if (comment.updated) {
+ output.updated = comment.updated;
+ }
+ if (comment.tag) {
+ output.tag = comment.tag;
+ }
+ return output;
+}
diff --git a/polygerrit-ui/app/utils/deep-util.ts b/polygerrit-ui/app/utils/deep-util.ts
index 7ed7dd4..eca528e 100644
--- a/polygerrit-ui/app/utils/deep-util.ts
+++ b/polygerrit-ui/app/utils/deep-util.ts
@@ -3,44 +3,76 @@
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+
+// NOTE: This algorithm has the following limitations:
+// It does not support deep-value-equality of values in sets that are not
+// `===`. The same applies for keys in a map.
export function deepEqual<T>(a: T, b: T): boolean {
- if (a === b) return true;
- if (a === undefined || b === undefined) return false;
- if (a === null || b === null) return false;
- if (a instanceof Date || b instanceof Date) {
- if (!(a instanceof Date && b instanceof Date)) return false;
- return a.getTime() === b.getTime();
- }
-
- if (a instanceof Set || b instanceof Set) {
- if (!(a instanceof Set && b instanceof Set)) return false;
- if (a.size !== b.size) return false;
- for (const ai of a) if (!b.has(ai)) return false;
- return true;
- }
- if (a instanceof Map || b instanceof Map) {
- if (!(a instanceof Map && b instanceof Map)) return false;
- if (a.size !== b.size) return false;
- for (const [aKey, aValue] of a.entries()) {
- if (!b.has(aKey) || !deepEqual(aValue, b.get(aKey))) return false;
+ // The pairs of objects that are currently being compared. If a pair is
+ // encountered again while on the stack, we shouldn't go any deeper, as we
+ // would only be walking through same pairs again infinitely. Such pairs are
+ // equal as long as all non-recursive pairs are equal, ie. given an infinite
+ // traversal we would've never reached a pair of values that are not equal to
+ // each other.
+ const onStackValuePair = new Map<unknown, Set<unknown>>();
+ // Cache of compared object instances. This allows as to avoid comparing same
+ // pair of large objects repeatedly in cases where the reference to the same
+ // object is stored in many different attributes in the tree.
+ const equalValues = new Map<unknown, Set<unknown>>();
+ function deepEqualImpl(a: unknown, b: unknown) {
+ if (a === b) return true;
+ if (a === undefined || b === undefined) return false;
+ if (a === null || b === null) return false;
+ if (a instanceof Date || b instanceof Date) {
+ if (!(a instanceof Date && b instanceof Date)) return false;
+ return a.getTime() === b.getTime();
}
- return true;
- }
- if (typeof a === 'object') {
- if (typeof b !== 'object') return false;
- const aObj = a as Record<string, unknown>;
- const bObj = b as Record<string, unknown>;
- const aKeys = Object.keys(aObj);
- const bKeys = Object.keys(bObj);
- if (aKeys.length !== bKeys.length) return false;
- for (const key of aKeys) {
- if (!deepEqual(aObj[key], bObj[key])) return false;
+ // Check cache first for container types.
+ if (equalValues?.get(a)?.has(b)) return true;
+
+ if (a instanceof Set || b instanceof Set) {
+ if (!(a instanceof Set && b instanceof Set)) return false;
+ if (a.size !== b.size) return false;
+ for (const ai of a) if (!b.has(ai)) return false;
+ equalValues.set(a, (equalValues.get(a) ?? new Set()).add(b));
+ return true;
}
- return true;
+ if (a instanceof Map || b instanceof Map) {
+ if (!(a instanceof Map && b instanceof Map)) return false;
+ if (a.size !== b.size) return false;
+ if (onStackValuePair.get(a)?.has(b)) return true;
+ onStackValuePair.set(a, (onStackValuePair.get(a) ?? new Set()).add(b));
+
+ for (const [aKey, aValue] of a.entries()) {
+ if (!b.has(aKey) || !deepEqualImpl(aValue, b.get(aKey))) return false;
+ }
+ onStackValuePair.get(a)!.delete(b);
+ equalValues.set(a, (equalValues.get(a) ?? new Set()).add(b));
+ return true;
+ }
+
+ if (typeof a === 'object') {
+ if (typeof b !== 'object') return false;
+ if (onStackValuePair.get(a)?.has(b)) return true;
+ onStackValuePair.set(a, (onStackValuePair.get(a) ?? new Set()).add(b));
+
+ const aObj = a as Record<string, unknown>;
+ const bObj = b as Record<string, unknown>;
+ const aKeys = Object.keys(aObj);
+ const bKeys = Object.keys(bObj);
+ if (aKeys.length !== bKeys.length) return false;
+ for (const key of aKeys) {
+ if (!deepEqualImpl(aObj[key], bObj[key])) return false;
+ }
+ onStackValuePair.get(a)!.delete(b);
+ equalValues.set(a, (equalValues.get(a) ?? new Set()).add(b));
+ return true;
+ }
+ return false;
}
- return false;
+ return deepEqualImpl(a, b);
}
export function notDeepEqual<T>(a: T, b: T): boolean {
diff --git a/polygerrit-ui/app/utils/deep-util_test.ts b/polygerrit-ui/app/utils/deep-util_test.ts
index c671c53..9e04fa1 100644
--- a/polygerrit-ui/app/utils/deep-util_test.ts
+++ b/polygerrit-ui/app/utils/deep-util_test.ts
@@ -39,6 +39,7 @@
assert.isFalse(deepEqual({}, null));
assert.isFalse(deepEqual({}, {x: 'y'}));
assert.isFalse(deepEqual({x: 'y'}, {x: 'z'}));
+ assert.isFalse(deepEqual({a: 'y'}, {b: 'y'}));
assert.isFalse(deepEqual({x: 'y'}, {z: 'y'}));
assert.isFalse(deepEqual({x: {y: 'y'}}, {x: {y: 'z'}}));
});
@@ -98,4 +99,69 @@
test('deepEqual nested', () => {
assert.isFalse(deepEqual({foo: new Set([])}, {foo: new Map([])}));
});
+
+ test('deepEqual recursive', () => {
+ const a = {};
+ const b = {a};
+ (a as any)['b'] = b;
+ const c = {};
+ const d = {a: c};
+ (c as any)['b'] = d;
+
+ assert.isTrue(deepEqual(a, c));
+ });
+
+ test('deepEqual map recursive', () => {
+ const a = new Map();
+ const b = {a};
+ a.set('b', b);
+
+ const c = new Map();
+ const d = {a: c};
+ c.set('b', d);
+
+ assert.isTrue(deepEqual(a, c));
+ });
+
+ test('deepEqual direct map recursive', () => {
+ const a = new Map();
+ const b = new Map();
+ b.set('a', a);
+ a.set('b', b);
+
+ const c = new Map();
+ const d = new Map();
+ d.set('a', c);
+ c.set('b', d);
+
+ assert.isTrue(deepEqual(a, c));
+ });
+
+ test('deepEqual direct self recursion', () => {
+ const a = {value: 3};
+ (a as any).self = a;
+ const b = {value: 3};
+ (b as any).self = b;
+
+ assert.isTrue(deepEqual(a, b));
+ });
+
+ test('deepEqual through of sets containing Symbols', () => {
+ const asymbol = Symbol('a');
+ const bsymbol = asymbol;
+
+ const a = new Set([asymbol]);
+ const b = new Set([bsymbol]);
+ assert.isTrue(deepEqual(a, b));
+ });
+
+ test('deepEqual recursively deeper', () => {
+ const a: {link?: any} = {};
+ const b: {link?: any} = {};
+ const c: {link?: any} = {};
+ a.link = b;
+ b.link = c;
+ c.link = a;
+ deepEqual(a, c);
+ });
});
diff --git a/polygerrit-ui/app/utils/message-util.ts b/polygerrit-ui/app/utils/message-util.ts
index 5acdf33..fffe612 100644
--- a/polygerrit-ui/app/utils/message-util.ts
+++ b/polygerrit-ui/app/utils/message-util.ts
@@ -7,7 +7,8 @@
import {ChangeId, ChangeMessageInfo} from '../types/common';
function getRevertChangeIdFromMessage(msg: ChangeMessageInfo): ChangeId {
- const REVERT_REGEX = /^Created a revert of this change as (.*)$/;
+ const REVERT_REGEX =
+ /^Created a revert of this change as .*?(I[0-9a-f]{40})$/;
const changeId = msg.message.match(REVERT_REGEX)?.[1];
if (!changeId) throw new Error('revert changeId not found');
return changeId as ChangeId;
diff --git a/polygerrit-ui/app/utils/message-util_test.ts b/polygerrit-ui/app/utils/message-util_test.ts
index 22a5e4d..64d765a 100644
--- a/polygerrit-ui/app/utils/message-util_test.ts
+++ b/polygerrit-ui/app/utils/message-util_test.ts
@@ -14,12 +14,8 @@
const messages = [
{
...createChangeMessage(),
- message: 'Created a revert of this change as 123',
- tag: MessageTag.TAG_REVERT as ReviewInputTag,
- },
- {
- ...createChangeMessage(),
- message: 'Created a revert of this change as xyz',
+ message:
+ 'Created a revert of this change as If02ca1cd494579d6bb92a157bf1819e3689cd6b1',
tag: MessageTag.TAG_REVERT as ReviewInputTag,
},
{
@@ -30,8 +26,27 @@
];
assert.deepEqual(getRevertCreatedChangeIds(messages), [
- '123' as ChangeId,
- 'xyz' as ChangeId,
+ 'If02ca1cd494579d6bb92a157bf1819e3689cd6b1' as ChangeId,
+ ]);
+ });
+
+ test('getRevertCreatedChangeIds with extra spam', () => {
+ const messages = [
+ {
+ ...createChangeMessage(),
+ message:
+ 'Created a revert of this change as IIf02ca1cd494579d6bb92a157bf1819e3689cd6b1',
+ tag: MessageTag.TAG_REVERT as ReviewInputTag,
+ },
+ {
+ ...createChangeMessage(),
+ message: 'Created a revert of this change as abc',
+ tag: undefined,
+ },
+ ];
+
+ assert.deepEqual(getRevertCreatedChangeIds(messages), [
+ 'If02ca1cd494579d6bb92a157bf1819e3689cd6b1' as ChangeId,
]);
});
});
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index 6ceaa4f..c44e907 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -3,7 +3,13 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {BasePatchSetNum, PARENT, RevisionPatchSetNum} from '../types/common';
+import {
+ AuthInfo,
+ BasePatchSetNum,
+ PARENT,
+ RevisionPatchSetNum,
+} from '../types/common';
+import {AuthType} from '../api/rest-api';
export function getBaseUrl(): string {
// window is not defined in service worker, therefore no CANONICAL_PATH
@@ -11,6 +17,37 @@
return self.CANONICAL_PATH || '';
}
+/**
+ * Return the url to use for login. If the server configuration
+ * contains the `loginUrl` in the `auth` section then that custom url
+ * will be used, defaults to `/login` otherwise.
+ *
+ * @param authConfig the auth section of gerrit configuration if defined
+ */
+export function loginUrl(authConfig: AuthInfo | undefined): string {
+ const baseUrl = getBaseUrl();
+ const customLoginUrl = authConfig?.login_url;
+ const authType = authConfig?.auth_type;
+ if (
+ customLoginUrl &&
+ (authType === AuthType.HTTP || authType === AuthType.HTTP_LDAP)
+ ) {
+ return customLoginUrl.startsWith('http')
+ ? customLoginUrl
+ : baseUrl + customLoginUrl;
+ } else {
+ // Strip the canonical path from the path since needing canonical in
+ // the path is unneeded and breaks the url.
+ const defaultUrl = `${baseUrl}/login/`;
+ const postFix = encodeURIComponent(
+ window.location.pathname.substring(baseUrl.length) +
+ window.location.search +
+ window.location.hash
+ );
+ return defaultUrl + postFix;
+ }
+}
+
export interface PatchRangeParams {
patchNum?: RevisionPatchSetNum;
basePatchNum?: BasePatchSetNum;
diff --git a/polygerrit-ui/app/utils/url-util_test.ts b/polygerrit-ui/app/utils/url-util_test.ts
index e2ff837..32b9da2 100644
--- a/polygerrit-ui/app/utils/url-util_test.ts
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -3,20 +3,22 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {BasePatchSetNum, RevisionPatchSetNum} from '../api/rest-api';
+import {AuthType, BasePatchSetNum, RevisionPatchSetNum} from '../api/rest-api';
import '../test/common-test-setup';
import {
- getBaseUrl,
encodeURL,
+ getBaseUrl,
+ getPatchRangeExpression,
+ loginUrl,
+ PatchRangeParams,
singleDecodeURL,
toPath,
toPathname,
toSearchParams,
- getPatchRangeExpression,
- PatchRangeParams,
sameOrigin,
} from './url-util';
import {assert} from '@open-wc/testing';
+import {createAuth} from '../test/test-data-generators';
suite('url-util tests', () => {
suite('getBaseUrl tests', () => {
@@ -36,6 +38,47 @@
});
});
+ suite('loginUrl tests', () => {
+ const authConfig = createAuth();
+
+ test('default url if auth.loginUrl is not defined', () => {
+ const current = encodeURIComponent(
+ window.location.pathname + window.location.search + window.location.hash
+ );
+ assert.deepEqual(loginUrl(undefined), '/login/' + current);
+ assert.deepEqual(loginUrl(authConfig), '/login/' + current);
+ });
+
+ test('default url if auth type is not HTTP or HTTP_LDAP', () => {
+ const defaultUrl =
+ '/login/' +
+ encodeURIComponent(
+ window.location.pathname +
+ window.location.search +
+ window.location.hash
+ );
+ const customLoginUrl = '/custom';
+ authConfig.login_url = customLoginUrl;
+
+ authConfig.auth_type = AuthType.LDAP;
+ assert.deepEqual(loginUrl(authConfig), defaultUrl);
+ authConfig.auth_type = AuthType.OPENID_SSO;
+ assert.deepEqual(loginUrl(authConfig), defaultUrl);
+ authConfig.auth_type = AuthType.OAUTH;
+ assert.deepEqual(loginUrl(authConfig), defaultUrl);
+ });
+
+ test('use auth.loginUrl when defined', () => {
+ const customLoginUrl = '/custom';
+ authConfig.login_url = customLoginUrl;
+
+ authConfig.auth_type = AuthType.HTTP;
+ assert.deepEqual(loginUrl(authConfig), customLoginUrl);
+ authConfig.auth_type = AuthType.HTTP_LDAP;
+ assert.deepEqual(loginUrl(authConfig), customLoginUrl);
+ });
+ });
+
suite('url encoding and decoding tests', () => {
suite('encodeURL', () => {
test('does not encode alphanumeric chars', () => {
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index 98ccffc..109eee2 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -10,7 +10,7 @@
"@open-wc/semantic-dom-diff": "^0.19.9",
"@open-wc/testing": "^3.2.0",
"@web/dev-server-esbuild": "^0.3.6",
- "@web/test-runner": "^0.15.3",
+ "@web/test-runner": "^0.14.0",
"@web/test-runner-playwright": "^0.9.0",
"@web/test-runner-visual-regression": "^0.7.1",
"accessibility-developer-tools": "^2.12.0",
@@ -34,4 +34,4 @@
},
"license": "Apache-2.0",
"private": true
-}
\ No newline at end of file
+}
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index 68e110e..aadc125 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -1302,20 +1302,6 @@
"@types/sinon-chai" "^3.2.3"
chai-a11y-axe "^1.5.0"
-"@puppeteer/browsers@0.5.0":
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-0.5.0.tgz#1a1ee454b84a986b937ca2d93146f25a3fe8b670"
- integrity sha512-Uw6oB7VvmPRLE4iKsjuOh8zgDabhNX67dzo8U/BB0f9527qx+4eeUs+korU98OhG5C4ubg7ufBgVi63XYwS6TQ==
- dependencies:
- debug "4.3.4"
- extract-zip "2.0.1"
- https-proxy-agent "5.0.1"
- progress "2.0.3"
- proxy-from-env "1.1.0"
- tar-fs "2.1.1"
- unbzip2-stream "1.4.3"
- yargs "17.7.1"
-
"@rollup/plugin-node-resolve@^13.0.4":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c"
@@ -1828,7 +1814,7 @@
resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
-"@web/browser-logs@^0.2.6":
+"@web/browser-logs@^0.2.2", "@web/browser-logs@^0.2.6":
version "0.2.6"
resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.2.6.tgz#ec936f78c7cf7b0ef9fb990c0097a3da1a756b20"
integrity sha512-CNjNVhd4FplRY8PPWIAt02vAowJAVcOoTNrR/NNb/o9pka7yI9qdjpWrWhEbPr2pOXonWb52AeAgdK66B8ZH7w==
@@ -1920,7 +1906,7 @@
rollup "^2.67.0"
whatwg-url "^11.0.0"
-"@web/dev-server@^0.1.38":
+"@web/dev-server@^0.1.35":
version "0.1.38"
resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.1.38.tgz#d755092d66aeb923c546237a6c460439ea3ddd29"
integrity sha512-WUq7Zi8KeJ5/UZmmpZ+kzUpUlFlMP/rcreJKYg9Lxiz998KYl4G5Rv24akX0piTuqXG7r6h+zszg8V/hdzjCoA==
@@ -1956,17 +1942,17 @@
"@types/parse5" "^6.0.1"
parse5 "^6.0.1"
-"@web/test-runner-chrome@^0.12.1":
- version "0.12.1"
- resolved "https://registry.yarnpkg.com/@web/test-runner-chrome/-/test-runner-chrome-0.12.1.tgz#3f4d7b83565807d75a84907ffd91ae1bd2298a52"
- integrity sha512-QxzinqYHelZQpMHAuc5TYyWVhtHUEGhL3m1p2U+mTTTWrZYX3D0s6Q0oL2+XYT1dsja5sd71h7yiBTb9ctkKOg==
+"@web/test-runner-chrome@^0.10.7":
+ version "0.10.7"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-chrome/-/test-runner-chrome-0.10.7.tgz#2dc35da47aa8b98c59f9e229a70ea3f443303e0c"
+ integrity sha512-DKJVHhHh3e/b6/erfKOy0a4kGfZ47qMoQRgROxi9T4F9lavEY3E5/MQ7hapHFM2lBF4vDrm+EWjtBdOL8o42tw==
dependencies:
- "@web/test-runner-core" "^0.10.29"
- "@web/test-runner-coverage-v8" "^0.5.0"
+ "@web/test-runner-core" "^0.10.20"
+ "@web/test-runner-coverage-v8" "^0.4.8"
chrome-launcher "^0.15.0"
- puppeteer-core "^19.8.1"
+ puppeteer-core "^13.1.3"
-"@web/test-runner-commands@^0.6.5", "@web/test-runner-commands@^0.6.6":
+"@web/test-runner-commands@^0.6.3", "@web/test-runner-commands@^0.6.5", "@web/test-runner-commands@^0.6.6":
version "0.6.6"
resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.6.6.tgz#e0e8c4ce6dcd91e5b18cf2212511ee6108e31070"
integrity sha512-2DcK/+7f8QTicQpGFq/TmvKHDK/6Zald6rn1zqRlmj3pcH8fX6KHNVMU60Za9QgAKdorMBPfd8dJwWba5otzdw==
@@ -1982,7 +1968,7 @@
"@web/test-runner-core" "^0.11.0"
mkdirp "^1.0.4"
-"@web/test-runner-core@^0.10.20", "@web/test-runner-core@^0.10.29":
+"@web/test-runner-core@^0.10.20", "@web/test-runner-core@^0.10.27", "@web/test-runner-core@^0.10.29":
version "0.10.29"
resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.10.29.tgz#d8d909c849151cf19013d6084f89a31e400557d5"
integrity sha512-0/ZALYaycEWswHhpyvl5yqo0uIfCmZe8q14nGPi1dMmNiqLcHjyFGnuIiLexI224AW74ljHcHllmDlXK9FUKGA==
@@ -2046,6 +2032,16 @@
picomatch "^2.2.2"
source-map "^0.7.3"
+"@web/test-runner-coverage-v8@^0.4.8":
+ version "0.4.9"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.4.9.tgz#334d80cd19fc68c08ec3339b1b1d2725078b51a2"
+ integrity sha512-y9LWL4uY25+fKQTljwr0XTYjeWIwU4h8eYidVuLoW3n1CdFkaddv+smrGzzF5j8XY+Mp6TmV9NdxjvMWqVkDdw==
+ dependencies:
+ "@web/test-runner-core" "^0.10.20"
+ istanbul-lib-coverage "^3.0.0"
+ picomatch "^2.2.2"
+ v8-to-istanbul "^8.0.0"
+
"@web/test-runner-coverage-v8@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.5.0.tgz#d1b033fd4baddaf5636a41cd017e321a338727a6"
@@ -2087,22 +2083,22 @@
pixelmatch "^5.2.1"
pngjs "^6.0.0"
-"@web/test-runner@^0.15.3":
- version "0.15.3"
- resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.15.3.tgz#8cf51293e5889c5db63c75fb7d422b5c820dcf01"
- integrity sha512-unwBymuQpI8yc/129K9H0aIzLIIQFrr2/mhdcIWFeZjjw5X3TJh57p5NFOA76nhlBSjFHyu0U0FXw9uOzXUCuQ==
+"@web/test-runner@^0.14.0":
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.14.1.tgz#a637e45c9b6ce7860ab780b5ac82dbfa1ed824f9"
+ integrity sha512-S2/Xp/bZBJdbWeTQxhs45cO9Khwqx99X+rrx8l0uDR0Ju/+kX+yC3RpjnOY1ooKD3rjkoEAE82soZTZNz+aKIg==
dependencies:
- "@web/browser-logs" "^0.2.6"
+ "@web/browser-logs" "^0.2.2"
"@web/config-loader" "^0.1.3"
- "@web/dev-server" "^0.1.38"
- "@web/test-runner-chrome" "^0.12.1"
- "@web/test-runner-commands" "^0.6.6"
- "@web/test-runner-core" "^0.10.29"
+ "@web/dev-server" "^0.1.35"
+ "@web/test-runner-chrome" "^0.10.7"
+ "@web/test-runner-commands" "^0.6.3"
+ "@web/test-runner-core" "^0.10.27"
"@web/test-runner-mocha" "^0.7.5"
camelcase "^6.2.0"
command-line-args "^5.1.1"
- command-line-usage "^7.0.1"
- convert-source-map "^2.0.0"
+ command-line-usage "^6.1.1"
+ convert-source-map "^1.7.0"
diff "^5.0.0"
globby "^11.0.1"
nanocolors "^0.2.1"
@@ -2561,13 +2557,6 @@
is-wsl "^2.2.0"
lighthouse-logger "^1.0.0"
-chromium-bidi@0.4.7:
- version "0.4.7"
- resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.4.7.tgz#4c022c2b0fb1d1c9b571fadf373042160e71d236"
- integrity sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==
- dependencies:
- mitt "3.0.0"
-
clean-css@^4.2.3:
version "4.2.4"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
@@ -2598,15 +2587,6 @@
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
-cliui@^8.0.1:
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
- integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.1"
- wrap-ansi "^7.0.0"
-
clone@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
@@ -2668,7 +2648,7 @@
lodash.camelcase "^4.3.0"
typical "^4.0.0"
-command-line-usage@^6.1.0:
+command-line-usage@^6.1.0, command-line-usage@^6.1.1:
version "6.1.3"
resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957"
integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==
@@ -2892,10 +2872,10 @@
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
-devtools-protocol@0.0.1107588:
- version "0.0.1107588"
- resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1107588.tgz#f8cac707840b97cc30b029359341bcbbb0ad8ffa"
- integrity sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==
+devtools-protocol@0.0.981744:
+ version "0.0.981744"
+ resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf"
+ integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==
di@^0.0.1:
version "0.0.1"
@@ -3282,6 +3262,14 @@
dependencies:
locate-path "^3.0.0"
+find-up@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
flat@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
@@ -4064,6 +4052,13 @@
p-locate "^3.0.0"
path-exists "^3.0.0"
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
@@ -4259,11 +4254,6 @@
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
-mitt@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
- integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
-
mkdirp-classic@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
@@ -4478,7 +4468,7 @@
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
-p-limit@^2.0.0:
+p-limit@^2.0.0, p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
@@ -4499,6 +4489,13 @@
dependencies:
p-limit "^2.0.0"
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
@@ -4633,6 +4630,13 @@
dependencies:
pngjs "^6.0.0"
+pkg-dir@4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+ integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ dependencies:
+ find-up "^4.0.0"
+
playwright-core@1.35.0:
version "1.35.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.0.tgz#b7871b742b4a5c8714b7fa2f570c280a061cb414"
@@ -4713,22 +4717,23 @@
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
-puppeteer-core@^19.8.1:
- version "19.11.1"
- resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.11.1.tgz#4c63d7a0a6cd268ff054ebcac315b646eee32667"
- integrity sha512-qcuC2Uf0Fwdj9wNtaTZ2OvYRraXpAK+puwwVW8ofOhOgLPZyz1c68tsorfIZyCUOpyBisjr+xByu7BMbEYMepA==
+puppeteer-core@^13.1.3:
+ version "13.7.0"
+ resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-13.7.0.tgz#3344bee3994163f49120a55ddcd144a40575ba5b"
+ integrity sha512-rXja4vcnAzFAP1OVLq/5dWNfwBGuzcOARJ6qGV7oAZhnLmVRU8G5MsdeQEAOy332ZhkIOnn9jp15R89LKHyp2Q==
dependencies:
- "@puppeteer/browsers" "0.5.0"
- chromium-bidi "0.4.7"
cross-fetch "3.1.5"
debug "4.3.4"
- devtools-protocol "0.0.1107588"
+ devtools-protocol "0.0.981744"
extract-zip "2.0.1"
https-proxy-agent "5.0.1"
+ pkg-dir "4.2.0"
+ progress "2.0.3"
proxy-from-env "1.1.0"
+ rimraf "3.0.2"
tar-fs "2.1.1"
unbzip2-stream "1.4.3"
- ws "8.13.0"
+ ws "8.5.0"
qjobs@^1.2.0:
version "1.2.0"
@@ -4948,7 +4953,7 @@
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
-rimraf@^3.0.0, rimraf@^3.0.2:
+rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
@@ -5168,7 +5173,7 @@
debug "^4.3.4"
fs-extra "^8.1.0"
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+string-width@^4.1.0, string-width@^4.2.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -5535,6 +5540,15 @@
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+v8-to-istanbul@^8.0.0:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed"
+ integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.1"
+ convert-source-map "^1.6.0"
+ source-map "^0.7.3"
+
v8-to-istanbul@^9.0.1:
version "9.1.0"
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265"
@@ -5676,10 +5690,10 @@
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
-ws@8.13.0:
- version "8.13.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
- integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
+ws@8.5.0:
+ version "8.5.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"
+ integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
ws@^7.4.2:
version "7.5.9"
@@ -5726,11 +5740,6 @@
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-yargs-parser@^21.1.1:
- version "21.1.1"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
- integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
-
yargs-unparser@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
@@ -5754,19 +5763,6 @@
y18n "^5.0.5"
yargs-parser "^20.2.2"
-yargs@17.7.1:
- version "17.7.1"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967"
- integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==
- dependencies:
- cliui "^8.0.1"
- escalade "^3.1.1"
- get-caller-file "^2.0.5"
- require-directory "^2.1.1"
- string-width "^4.2.3"
- y18n "^5.0.5"
- yargs-parser "^21.1.1"
-
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
diff --git a/proto/cache.proto b/proto/cache.proto
index 79bfa9e..7e38d92 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -252,7 +252,7 @@
// Next ID: 3
message TagProto {
bytes id = 1;
- bytes flags = 2;
+ bytes flags = 2; // org.roaringbitmap.RoaringBitmap serialized as ByteString
}
repeated TagProto tag = 3;
}
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index dbfef44..b5892794 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -48,14 +48,6 @@
// Disable extra font load from paper-styles
window.polymerSkipLoadingFontRoboto = true;
window.CLOSURE_NO_DEPS = true;
- window.DEFAULT_DETAIL_HEXES = {lb}
- {if $defaultChangeDetailHex}
- changePage: '{$defaultChangeDetailHex}',
- {/if}
- {if $defaultDashboardHex}
- dashboardPage: '{$defaultDashboardHex}',
- {/if}
- {rb};
window.PRELOADED_QUERIES = {lb}
{if $userIsAuthenticated and $defaultDashboardHex and $dashboardQuery}
dashboardQuery: [{for $query in $dashboardQuery}{$query},{/for}],
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeader.soy b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
index 027d78b3d..12b68b6 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeader.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
@@ -17,8 +17,8 @@
{namespace com.google.gerrit.server.mail.template.ChangeHeader}
{template ChangeHeader kind="text"}
- {@param attentionSet: ?}
- {if $attentionSet}
+ {@param attentionSet: list<string>|null}
+ {if $attentionSet and length($attentionSet) > 0}
Attention is currently required from:{sp}
{for $attentionSetUser, $index in $attentionSet}
{$attentionSetUser}
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
index 0d8da38..e17e021 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
@@ -18,8 +18,8 @@
{namespace com.google.gerrit.server.mail.template.ChangeHeaderHtml}
{template ChangeHeaderHtml}
- {@param attentionSet: ?}
- {if $attentionSet}
+ {@param attentionSet: list<string>|null}
+ {if $attentionSet and length($attentionSet) > 0}
<p> Attention is currently required from:{sp}
{for $attentionSetUser, $index in $attentionSet}
{$attentionSetUser}
diff --git a/resources/com/google/gerrit/server/mail/Private.soy b/resources/com/google/gerrit/server/mail/Private.soy
index 7920c21..be1f79b 100644
--- a/resources/com/google/gerrit/server/mail/Private.soy
+++ b/resources/com/google/gerrit/server/mail/Private.soy
@@ -38,7 +38,7 @@
// monospace text.
white-space: pre-wrap;
{/let}
- <pre style="{$preStyle}">{$content|changeNewlineToBr}</pre>
+ <pre class="blocks" style="{$preStyle}">{$content|changeNewlineToBr}</pre>
{/template}
/**
@@ -69,15 +69,15 @@
{for $block in $content}
{if $block.type == 'paragraph'}
- <p style="{$pStyle}">{$block.text|changeNewlineToBr}</p>
+ <p class="blocks" style="{$pStyle}">{$block.text|changeNewlineToBr}</p>
{elseif $block.type == 'quote'}
- <blockquote style="{$blockquoteStyle}">
+ <blockquote class="blocks" style="{$blockquoteStyle}">
{call WikiFormat}{param content: $block.quotedBlocks /}{/call}
</blockquote>
{elseif $block.type == 'pre'}
{call Pre}{param content: $block.text /}{/call}
{elseif $block.type == 'list'}
- <ul>
+ <ul class="blocks">
{for $item in $block.items}
<li>{$item}</li>
{/for}
diff --git a/tools/BUILD b/tools/BUILD
index e05a705..70d4315 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -159,6 +159,7 @@
"-Xep:FloatingPointLiteralPrecision:ERROR",
"-Xep:FloggerArgumentToString:ERROR",
"-Xep:FloggerFormatString:ERROR",
+ "-Xep:FloggerLogString:WARN",
"-Xep:FloggerLogVarargs:ERROR",
"-Xep:FloggerSplitLogStatement:ERROR",
"-Xep:FloggerStringConcatenation:ERROR",
diff --git a/tools/deps.bzl b/tools/deps.bzl
index 592e4a7..4293d7c 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -22,6 +22,7 @@
HTTPCOMP_VERS = "4.5.2"
JETTY_VERS = "9.4.51.v20230217"
BYTE_BUDDY_VERSION = "1.10.7"
+ROARING_BITMAP_VERSION = "0.9.44"
def java_dependencies():
maven_jar(
@@ -733,3 +734,15 @@
artifact = "org.objenesis:objenesis:3.0.1",
sha1 = "11cfac598df9dc48bb9ed9357ed04212694b7808",
)
+
+ maven_jar(
+ name = "roaringbitmap",
+ artifact = "org.roaringbitmap:RoaringBitmap:" + ROARING_BITMAP_VERSION,
+ sha1 = "d25b4bcb67193d587f6e0617da2c6f84e2d02a9c",
+ )
+
+ maven_jar(
+ name = "roaringbitmap-shims",
+ artifact = "org.roaringbitmap:shims:" + ROARING_BITMAP_VERSION,
+ sha1 = "e22be0d690a99c046bf9f57106065a77edad1eda",
+ )
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index 3765575..afefbb2 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -18,7 +18,7 @@
"rollup-plugin-define": "^1.0.1",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^5.1.3",
- "typescript": "^4.7.2"
+ "typescript": "^4.9.5"
},
"devDependencies": {},
"license": "Apache-2.0",
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index c6b3eab..92afd0d 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -1951,10 +1951,10 @@
dependencies:
tslib "^1.8.1"
-typescript@^4.7.2:
- version "4.7.2"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4"
- integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==
+typescript@^4.9.5:
+ version "4.9.5"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
+ integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typical@^2.6.0, typical@^2.6.1:
version "2.6.1"