Merge "Fix two tiny issues with autocompletion logging"
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 8b6049e..ab77d1b 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -1137,6 +1137,45 @@
----
+[[highlightjs-epp]]
+highlightjs-epp
+
+* highlightjs-epp
+
+[[highlightjs-epp_license]]
+----
+BSD 3-Clause License
+
+Copyright (c) 2024, highlight.js
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
[[highlightjs-structured-text]]
highlightjs-structured-text
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 7e3fa9a..91b6227 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -4021,6 +4021,45 @@
----
+[[highlightjs-epp]]
+highlightjs-epp
+
+* highlightjs-epp
+
+[[highlightjs-epp_license]]
+----
+BSD 3-Clause License
+
+Copyright (c) 2024, highlight.js
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
[[highlightjs-structured-text]]
highlightjs-structured-text
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 50885b6..dd2c3de 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7224,12 +7224,10 @@
Number of inserted lines.
|`deletions` ||
Number of deleted lines.
-|`total_comment_count` |optional|
-Total number of inline comments across all patch sets. Not set if the current
-change index doesn't have the data.
-|`unresolved_comment_count` |optional|
-Number of unresolved inline comment threads across all patch sets. Not set if
-the current change index doesn't have the data.
+|`total_comment_count` ||
+Total number of inline comments across all patch sets.
+|`unresolved_comment_count` ||
+Number of unresolved inline comment threads across all patch sets.
|`_number` ||
The change number. (The underscore is just a relict of a prior
attempt to deprecate the change number.)
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index ab78ff0..a21ed8e 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -984,6 +984,68 @@
}
----
+[[create-config-change]]
+=== Create Config Change for review.
+--
+'PUT /projects/link:#project-name[\{project-name\}]/config:review'
+--
+
+Sets the configuration of a project.
+
+This takes the same input as link:#set-config[Set Config], but creates a pending
+change for review. Like link:#create-change[Create Change], it returns
+a link:#change-info[ChangeInfo] entity describing the resulting change.
+
+.Request
+----
+ PUT /projects/myproject/config:review HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "description": "demo project",
+ "use_contributor_agreements": "FALSE",
+ "use_content_merge": "INHERIT",
+ "use_signed_off_by": "INHERIT",
+ "create_new_change_for_all_not_in_target": "INHERIT",
+ "enable_signed_push": "INHERIT",
+ "require_signed_push": "INHERIT",
+ "reject_implicit_merges": "INHERIT",
+ "require_change_id": "TRUE",
+ "max_object_size_limit": "10m",
+ "submit_type": "REBASE_IF_NECESSARY",
+ "state": "ACTIVE"
+ }
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "testproj~refs%2Fmeta%2Fconfig~Ieaf185bf90a1fc3b58461e399385e158a20b31a2",
+ "project": "testproj",
+ "branch": "refs/meta/config",
+ "hashtags": [],
+ "change_id": "Ieaf185bf90a1fc3b58461e399385e158a20b31a2",
+ "subject": "Review access change",
+ "status": "NEW",
+ "created": "2017-09-07 14:31:11.852000000",
+ "updated": "2017-09-07 14:31:11.852000000",
+ "submit_type": "CHERRY_PICK",
+ "mergeable": true,
+ "insertions": 2,
+ "deletions": 0,
+ "unresolved_comment_count": 0,
+ "has_review_started": true,
+ "_number": 7,
+ "owner": {
+ "_account_id": 1000000
+ }
+ }
+----
+
[[run-gc]]
=== Run GC
--
@@ -4171,11 +4233,13 @@
Whether empty commits should be rejected when a change is merged.
Can be `TRUE`, `FALSE` or `INHERIT`. +
If not set, this setting is not updated.
-|commentlinks |optional|
+|`commentlinks` |optional|
Map of commentlink names to link:#commentlink-input[CommentLinkInput]
entities to add or update on the project. If the given commentlink
already exists, it will be updated with the given values, otherwise
it will be created. If the value is null, that entry is deleted.
+|`message` |optional|
+A commit message for this change.
|======================================================
[[config-parameter-info]]
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 96a6d32..cd805d0 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1550,6 +1550,17 @@
assertThat(res).isEqualTo(expectedContent);
}
+ protected void assertLastCommitAuthorAndShortMessage(
+ String refName, String expectedAuthor, String expectedShortMessage) throws Exception {
+ try (Repository repo = repoManager.openRepository(project);
+ RevWalk rw = new RevWalk(repo)) {
+ Ref exactRef = repo.exactRef(refName);
+ RevCommit revCommit = rw.parseCommit(exactRef.getObjectId());
+ assertThat(revCommit.getAuthorIdent().getName()).isEqualTo(expectedAuthor);
+ assertThat(revCommit.getShortMessage()).isEqualTo(expectedShortMessage);
+ }
+ }
+
@CanIgnoreReturnValue
protected RevCommit createNewCommitWithoutChangeId(String branch, String file, String content)
throws Exception {
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index 3ad7e03..51585f3 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -18,6 +18,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.ConvertibleToProto;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gson.Gson;
@@ -104,6 +105,7 @@
/** The numeric change ID */
@AutoValue
+ @ConvertibleToProto
public abstract static class Id {
/**
* Parse a Change.Id out of a string representation.
@@ -271,6 +273,7 @@
* "Ixxxxxx...", and is stored in the Change-Id footer of a commit.
*/
@AutoValue
+ @ConvertibleToProto
public abstract static class Key {
// TODO(dborowitz): This hardly seems worth it: why would someone pass a URL-encoded change key?
// Ideally the standard key() factory method would enforce the format and throw IAE.
diff --git a/java/com/google/gerrit/entities/ChangeMessage.java b/java/com/google/gerrit/entities/ChangeMessage.java
index dea070f..c8fc7d2 100644
--- a/java/com/google/gerrit/entities/ChangeMessage.java
+++ b/java/com/google/gerrit/entities/ChangeMessage.java
@@ -15,6 +15,7 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.ConvertibleToProto;
import com.google.gerrit.common.Nullable;
import java.time.Instant;
import java.util.Objects;
@@ -34,6 +35,7 @@
}
@AutoValue
+ @ConvertibleToProto
public abstract static class Key {
public abstract Change.Id changeId();
diff --git a/java/com/google/gerrit/entities/LabelId.java b/java/com/google/gerrit/entities/LabelId.java
index 2426818..e3b3024 100644
--- a/java/com/google/gerrit/entities/LabelId.java
+++ b/java/com/google/gerrit/entities/LabelId.java
@@ -15,8 +15,10 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.ConvertibleToProto;
@AutoValue
+@ConvertibleToProto
public abstract class LabelId {
public static final String LEGACY_SUBMIT_NAME = "SUBM";
public static final String CODE_REVIEW = "Code-Review";
diff --git a/java/com/google/gerrit/entities/NotifyConfig.java b/java/com/google/gerrit/entities/NotifyConfig.java
index d3123c4..42baebc 100644
--- a/java/com/google/gerrit/entities/NotifyConfig.java
+++ b/java/com/google/gerrit/entities/NotifyConfig.java
@@ -19,6 +19,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.ConvertibleToProto;
import com.google.gerrit.common.Nullable;
import java.util.EnumSet;
import java.util.Set;
@@ -31,6 +32,7 @@
BCC
}
+ @ConvertibleToProto
public enum NotifyType {
// sort by name, except 'ALL' which should stay last
ABANDONED_CHANGES,
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
index e8759fa..6f71874 100644
--- a/java/com/google/gerrit/entities/PatchSet.java
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -23,6 +23,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import com.google.errorprone.annotations.InlineMe;
+import com.google.gerrit.common.ConvertibleToProto;
import com.google.gerrit.common.Nullable;
import java.time.Instant;
import java.util.List;
@@ -31,6 +32,7 @@
/** A single revision of a {@link Change}. */
@AutoValue
+@ConvertibleToProto
public abstract class PatchSet {
/** Is the reference name a change reference? */
public static boolean isChangeRef(String name) {
@@ -67,6 +69,7 @@
}
@AutoValue
+ @ConvertibleToProto
public abstract static class Id implements Comparable<Id> {
/** Parse a PatchSet.Id out of a string representation. */
public static Id parse(String str) {
diff --git a/java/com/google/gerrit/entities/PatchSetApproval.java b/java/com/google/gerrit/entities/PatchSetApproval.java
index 608cf0d..f78167b 100644
--- a/java/com/google/gerrit/entities/PatchSetApproval.java
+++ b/java/com/google/gerrit/entities/PatchSetApproval.java
@@ -16,6 +16,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.primitives.Shorts;
+import com.google.gerrit.common.ConvertibleToProto;
import java.time.Instant;
import java.util.Optional;
@@ -27,6 +28,7 @@
}
@AutoValue
+ @ConvertibleToProto
public abstract static class Key {
public abstract PatchSet.Id patchSetId();
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index 9c2866c..7b02597 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -20,6 +20,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.common.ConvertibleToProto;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ProjectState;
@@ -57,6 +58,7 @@
* <p>This class is immutable and thread safe.
*/
@Immutable
+ @ConvertibleToProto
public static class NameKey implements Serializable, Comparable<NameKey> {
private static final long serialVersionUID = 1L;
diff --git a/java/com/google/gerrit/entities/converter/ChangeIdProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeIdProtoConverter.java
index 0d4ec70..909b4d3 100644
--- a/java/com/google/gerrit/entities/converter/ChangeIdProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeIdProtoConverter.java
@@ -17,10 +17,11 @@
import com.google.errorprone.annotations.Immutable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.Change_Id;
import com.google.protobuf.Parser;
@Immutable
-public enum ChangeIdProtoConverter implements ProtoConverter<Entities.Change_Id, Change.Id> {
+public enum ChangeIdProtoConverter implements SafeProtoConverter<Entities.Change_Id, Change.Id> {
INSTANCE;
@Override
@@ -37,4 +38,14 @@
public Parser<Entities.Change_Id> getParser() {
return Entities.Change_Id.parser();
}
+
+ @Override
+ public Class<Change_Id> getProtoClass() {
+ return Change_Id.class;
+ }
+
+ @Override
+ public Class<Change.Id> getEntityClass() {
+ return Change.Id.class;
+ }
}
diff --git a/java/com/google/gerrit/entities/converter/ChangeKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeKeyProtoConverter.java
index f3ccdfa..0620c70 100644
--- a/java/com/google/gerrit/entities/converter/ChangeKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeKeyProtoConverter.java
@@ -17,10 +17,11 @@
import com.google.errorprone.annotations.Immutable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.Change_Key;
import com.google.protobuf.Parser;
@Immutable
-public enum ChangeKeyProtoConverter implements ProtoConverter<Entities.Change_Key, Change.Key> {
+public enum ChangeKeyProtoConverter implements SafeProtoConverter<Entities.Change_Key, Change.Key> {
INSTANCE;
@Override
@@ -37,4 +38,14 @@
public Parser<Entities.Change_Key> getParser() {
return Entities.Change_Key.parser();
}
+
+ @Override
+ public Class<Change_Key> getProtoClass() {
+ return Change_Key.class;
+ }
+
+ @Override
+ public Class<Change.Key> getEntityClass() {
+ return Change.Key.class;
+ }
}
diff --git a/java/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverter.java
index 3e93c5a..a76ab98 100644
--- a/java/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeMessageKeyProtoConverter.java
@@ -18,11 +18,12 @@
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.ChangeMessage_Key;
import com.google.protobuf.Parser;
@Immutable
public enum ChangeMessageKeyProtoConverter
- implements ProtoConverter<Entities.ChangeMessage_Key, ChangeMessage.Key> {
+ implements SafeProtoConverter<Entities.ChangeMessage_Key, ChangeMessage.Key> {
INSTANCE;
private final ProtoConverter<Entities.Change_Id, Change.Id> changeIdConverter =
@@ -45,4 +46,14 @@
public Parser<Entities.ChangeMessage_Key> getParser() {
return Entities.ChangeMessage_Key.parser();
}
+
+ @Override
+ public Class<ChangeMessage_Key> getProtoClass() {
+ return ChangeMessage_Key.class;
+ }
+
+ @Override
+ public Class<ChangeMessage.Key> getEntityClass() {
+ return ChangeMessage.Key.class;
+ }
}
diff --git a/java/com/google/gerrit/entities/converter/LabelIdProtoConverter.java b/java/com/google/gerrit/entities/converter/LabelIdProtoConverter.java
index a1894ac..e6e1be7f 100644
--- a/java/com/google/gerrit/entities/converter/LabelIdProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/LabelIdProtoConverter.java
@@ -20,7 +20,7 @@
import com.google.protobuf.Parser;
@Immutable
-public enum LabelIdProtoConverter implements ProtoConverter<Entities.LabelId, LabelId> {
+public enum LabelIdProtoConverter implements SafeProtoConverter<Entities.LabelId, LabelId> {
INSTANCE;
@Override
@@ -37,4 +37,14 @@
public Parser<Entities.LabelId> getParser() {
return Entities.LabelId.parser();
}
+
+ @Override
+ public Class<Entities.LabelId> getProtoClass() {
+ return Entities.LabelId.class;
+ }
+
+ @Override
+ public Class<LabelId> getEntityClass() {
+ return LabelId.class;
+ }
}
diff --git a/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java b/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java
index 201dd78..fc963df 100644
--- a/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/NotifyInfoProtoConverter.java
@@ -26,7 +26,8 @@
* com.google.gerrit.proto.Entities.NotifyInfo}.
*/
@Immutable
-public enum NotifyInfoProtoConverter implements ProtoConverter<Entities.NotifyInfo, NotifyInfo> {
+public enum NotifyInfoProtoConverter
+ implements SafeProtoConverter<Entities.NotifyInfo, NotifyInfo> {
INSTANCE;
@Override
@@ -47,4 +48,14 @@
public Parser<Entities.NotifyInfo> getParser() {
return Entities.NotifyInfo.parser();
}
+
+ @Override
+ public Class<Entities.NotifyInfo> getProtoClass() {
+ return Entities.NotifyInfo.class;
+ }
+
+ @Override
+ public Class<NotifyInfo> getEntityClass() {
+ return NotifyInfo.class;
+ }
}
diff --git a/java/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverter.java
index c7d1714..3ea14e6 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetApprovalKeyProtoConverter.java
@@ -20,11 +20,12 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.PatchSetApproval_Key;
import com.google.protobuf.Parser;
@Immutable
public enum PatchSetApprovalKeyProtoConverter
- implements ProtoConverter<Entities.PatchSetApproval_Key, PatchSetApproval.Key> {
+ implements SafeProtoConverter<Entities.PatchSetApproval_Key, PatchSetApproval.Key> {
INSTANCE;
private final ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
@@ -55,4 +56,14 @@
public Parser<Entities.PatchSetApproval_Key> getParser() {
return Entities.PatchSetApproval_Key.parser();
}
+
+ @Override
+ public Class<PatchSetApproval_Key> getProtoClass() {
+ return PatchSetApproval_Key.class;
+ }
+
+ @Override
+ public Class<PatchSetApproval.Key> getEntityClass() {
+ return PatchSetApproval.Key.class;
+ }
}
diff --git a/java/com/google/gerrit/entities/converter/PatchSetIdProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetIdProtoConverter.java
index 60c13f1..f6671cf 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetIdProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetIdProtoConverter.java
@@ -18,10 +18,12 @@
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.PatchSet_Id;
import com.google.protobuf.Parser;
@Immutable
-public enum PatchSetIdProtoConverter implements ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> {
+public enum PatchSetIdProtoConverter
+ implements SafeProtoConverter<Entities.PatchSet_Id, PatchSet.Id> {
INSTANCE;
private final ProtoConverter<Entities.Change_Id, Change.Id> changeIdConverter =
@@ -44,4 +46,14 @@
public Parser<Entities.PatchSet_Id> getParser() {
return Entities.PatchSet_Id.parser();
}
+
+ @Override
+ public Class<PatchSet_Id> getProtoClass() {
+ return PatchSet_Id.class;
+ }
+
+ @Override
+ public Class<PatchSet.Id> getEntityClass() {
+ return PatchSet.Id.class;
+ }
}
diff --git a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
index 196deca..22985d9 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
@@ -24,7 +24,7 @@
import org.eclipse.jgit.lib.ObjectId;
@Immutable
-public enum PatchSetProtoConverter implements ProtoConverter<Entities.PatchSet, PatchSet> {
+public enum PatchSetProtoConverter implements SafeProtoConverter<Entities.PatchSet, PatchSet> {
INSTANCE;
private final ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
@@ -103,4 +103,14 @@
public Parser<Entities.PatchSet> getParser() {
return Entities.PatchSet.parser();
}
+
+ @Override
+ public Class<Entities.PatchSet> getProtoClass() {
+ return Entities.PatchSet.class;
+ }
+
+ @Override
+ public Class<PatchSet> getEntityClass() {
+ return PatchSet.class;
+ }
}
diff --git a/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
index 6bb0f79..320b8fc 100644
--- a/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
@@ -16,12 +16,14 @@
import com.google.errorprone.annotations.Immutable;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.Project_NameKey;
import com.google.protobuf.Parser;
@Immutable
public enum ProjectNameKeyProtoConverter
- implements ProtoConverter<Entities.Project_NameKey, Project.NameKey> {
+ implements SafeProtoConverter<Entities.Project_NameKey, Project.NameKey> {
INSTANCE;
@Override
@@ -38,4 +40,14 @@
public Parser<Entities.Project_NameKey> getParser() {
return Entities.Project_NameKey.parser();
}
+
+ @Override
+ public Class<Project_NameKey> getProtoClass() {
+ return Project_NameKey.class;
+ }
+
+ @Override
+ public Class<NameKey> getEntityClass() {
+ return NameKey.class;
+ }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java b/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
index dd29635..21bf886 100644
--- a/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
+++ b/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
@@ -15,10 +15,12 @@
package com.google.gerrit.extensions.api.changes;
import com.google.common.base.MoreObjects;
+import com.google.gerrit.common.ConvertibleToProto;
import java.util.List;
import java.util.Objects;
/** Detailed information about who should be notified about an update. */
+@ConvertibleToProto
public class NotifyInfo {
public List<String> accounts;
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
index 906fc4c..805e769 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -40,4 +40,5 @@
public ProjectState state;
public Map<String, Map<String, ConfigValue>> pluginConfigValues;
public Map<String, CommentLinkInput> commentLinks;
+ public String commitMessage;
}
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 0b1b6b0..697561b 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -59,6 +59,9 @@
@CanIgnoreReturnValue
ConfigInfo config(ConfigInput in) throws RestApiException;
+ @CanIgnoreReturnValue
+ ChangeInfo configReview(ConfigInput in) throws RestApiException;
+
Map<String, Set<String>> commitsIn(Collection<String> commits, Collection<String> refs)
throws RestApiException;
@@ -345,6 +348,11 @@
}
@Override
+ public ChangeInfo configReview(ConfigInput in) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public Map<String, Set<String>> commitsIn(Collection<String> commits, Collection<String> refs)
throws RestApiException {
throw new NotImplementedException();
diff --git a/java/com/google/gerrit/server/account/ProjectWatches.java b/java/com/google/gerrit/server/account/ProjectWatches.java
index 86132d3..341b493 100644
--- a/java/com/google/gerrit/server/account/ProjectWatches.java
+++ b/java/com/google/gerrit/server/account/ProjectWatches.java
@@ -29,6 +29,7 @@
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
+import com.google.gerrit.common.ConvertibleToProto;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.NotifyConfig;
@@ -79,6 +80,7 @@
*/
public class ProjectWatches {
@AutoValue
+ @ConvertibleToProto
public abstract static class ProjectWatchKey {
public static ProjectWatchKey create(Project.NameKey project, @Nullable String filter) {
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 5c24ddc..38b2d46 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -88,6 +88,7 @@
import com.google.gerrit.server.restapi.project.PostLabels;
import com.google.gerrit.server.restapi.project.ProjectsCollection;
import com.google.gerrit.server.restapi.project.PutConfig;
+import com.google.gerrit.server.restapi.project.PutConfigReview;
import com.google.gerrit.server.restapi.project.PutDescription;
import com.google.gerrit.server.restapi.project.SetAccess;
import com.google.gerrit.server.restapi.project.SetHead;
@@ -127,6 +128,7 @@
private final CreateAccessChange createAccessChange;
private final GetConfig getConfig;
private final PutConfig putConfig;
+ private final PutConfigReview putConfigReview;
private final CommitsIncludedInRefs commitsIncludedInRefs;
private final Provider<ListBranches> listBranches;
private final Provider<ListTags> listTags;
@@ -168,6 +170,7 @@
CreateAccessChange createAccessChange,
GetConfig getConfig,
PutConfig putConfig,
+ PutConfigReview putConfigReview,
CommitsIncludedInRefs commitsIncludedInRefs,
Provider<ListBranches> listBranches,
Provider<ListTags> listTags,
@@ -208,6 +211,7 @@
createAccessChange,
getConfig,
putConfig,
+ putConfigReview,
commitsIncludedInRefs,
listBranches,
listTags,
@@ -252,6 +256,7 @@
CreateAccessChange createAccessChange,
GetConfig getConfig,
PutConfig putConfig,
+ PutConfigReview putConfigReview,
CommitsIncludedInRefs commitsIncludedInRefs,
Provider<ListBranches> listBranches,
Provider<ListTags> listTags,
@@ -292,6 +297,7 @@
createAccessChange,
getConfig,
putConfig,
+ putConfigReview,
commitsIncludedInRefs,
listBranches,
listTags,
@@ -335,6 +341,7 @@
CreateAccessChange createAccessChange,
GetConfig getConfig,
PutConfig putConfig,
+ PutConfigReview putConfigReview,
CommitsIncludedInRefs commitsIncludedInRefs,
Provider<ListBranches> listBranches,
Provider<ListTags> listTags,
@@ -375,6 +382,7 @@
this.setAccess = setAccess;
this.getConfig = getConfig;
this.putConfig = putConfig;
+ this.putConfigReview = putConfigReview;
this.commitsIncludedInRefs = commitsIncludedInRefs;
this.listBranches = listBranches;
this.listTags = listTags;
@@ -519,6 +527,15 @@
}
@Override
+ public ChangeInfo configReview(ConfigInput p) throws RestApiException {
+ try {
+ return putConfigReview.apply(checkExists(), p).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot put config change", e);
+ }
+ }
+
+ @Override
public Map<String, Set<String>> commitsIn(Collection<String> commits, Collection<String> refs)
throws RestApiException {
try {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 3e8002b..3a50275 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -14,10 +14,8 @@
package com.google.gerrit.server.restapi.project;
-import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.AccessSection;
-import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.InvalidNameException;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
@@ -27,9 +25,10 @@
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.change.ChangeJson;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
import com.google.gerrit.server.update.UpdateException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -39,23 +38,18 @@
@Singleton
public class CreateAccessChange implements RestModifyView<ProjectResource, ProjectAccessInput> {
private final SetAccessUtil setAccess;
- private final ChangeJson.Factory jsonFactory;
private final RepoMetaDataUpdater repoMetaDataUpdater;
@Inject
- CreateAccessChange(
- SetAccessUtil accessUtil,
- ChangeJson.Factory jsonFactory,
- RepoMetaDataUpdater repoMetaDataUpdater) {
+ CreateAccessChange(SetAccessUtil accessUtil, RepoMetaDataUpdater repoMetaDataUpdater) {
this.setAccess = accessUtil;
- this.jsonFactory = jsonFactory;
this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
public Response<ChangeInfo> apply(ProjectResource rsrc, ProjectAccessInput input)
- throws PermissionBackendException, IOException, ConfigInvalidException, InvalidNameException,
- UpdateException, RestApiException {
+ throws PermissionBackendException, IOException, ConfigInvalidException, UpdateException,
+ RestApiException {
ImmutableList<AccessSection> removals =
setAccess.getAccessSections(input.remove, /* rejectNonResolvableGroups= */ false);
ImmutableList<AccessSection> additions =
@@ -63,28 +57,23 @@
Project.NameKey newParentProjectName =
input.parent == null ? null : Project.nameKey(input.parent);
- String message = !Strings.isNullOrEmpty(input.message) ? input.message : "Review access change";
- try {
- Change change =
- repoMetaDataUpdater.updateAndCreateChangeForReview(
- rsrc.getNameKey(),
- rsrc.getUser(),
- message,
- config -> {
- setAccess.validateChanges(config, removals, additions);
- setAccess.applyChanges(config, removals, additions);
- try {
- setAccess.setParentName(
- rsrc.getUser().asIdentifiedUser(),
- config,
- rsrc.getNameKey(),
- newParentProjectName,
- false);
- } catch (AuthException e) {
- throw new IllegalStateException(e);
- }
- });
- return Response.created(jsonFactory.noOptions().format(change));
+ try (ConfigChangeCreator creator =
+ repoMetaDataUpdater.configChangeCreator(
+ rsrc.getNameKey(), input.message, "Review access change")) {
+ ProjectConfig config = creator.getConfig();
+ setAccess.validateChanges(config, removals, additions);
+ setAccess.applyChanges(config, removals, additions);
+ try {
+ setAccess.setParentName(
+ rsrc.getUser().asIdentifiedUser(),
+ config,
+ rsrc.getNameKey(),
+ newParentProjectName,
+ false);
+ } catch (AuthException e) {
+ throw new IllegalStateException(e);
+ }
+ return creator.createChange();
} catch (InvalidNameException e) {
throw new BadRequestException(e.toString());
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
index a233834..12d1189 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -27,19 +27,13 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-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.project.LabelDefinitionJson;
import com.google.gerrit.server.project.LabelResource;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.List;
@@ -49,27 +43,14 @@
@Singleton
public class CreateLabel
implements RestCollectionCreateView<ProjectResource, LabelResource, LabelDefinitionInput> {
- private final Provider<CurrentUser> user;
- private final PermissionBackend permissionBackend;
- private final MetaDataUpdate.User updateFactory;
- private final ProjectConfig.Factory projectConfigFactory;
- private final ProjectCache projectCache;
private final ApprovalQueryBuilder approvalQueryBuilder;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
@Inject
public CreateLabel(
- Provider<CurrentUser> user,
- PermissionBackend permissionBackend,
- MetaDataUpdate.User updateFactory,
- ProjectConfig.Factory projectConfigFactory,
- ProjectCache projectCache,
- ApprovalQueryBuilder approvalQueryBuilder) {
- this.user = user;
- this.permissionBackend = permissionBackend;
- this.updateFactory = updateFactory;
- this.projectConfigFactory = projectConfigFactory;
- this.projectCache = projectCache;
+ ApprovalQueryBuilder approvalQueryBuilder, RepoMetaDataUpdater repoMetaDataUpdater) {
this.approvalQueryBuilder = approvalQueryBuilder;
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
@@ -77,15 +58,6 @@
ProjectResource rsrc, IdString id, LabelDefinitionInput input)
throws AuthException, BadRequestException, ResourceConflictException,
PermissionBackendException, IOException, ConfigInvalidException {
- if (!user.get().isIdentifiedUser()) {
- throw new AuthException("Authentication required");
- }
-
- permissionBackend
- .currentUser()
- .project(rsrc.getNameKey())
- .check(ProjectPermission.WRITE_CONFIG);
-
if (input == null) {
input = new LabelDefinitionInput();
}
@@ -93,22 +65,10 @@
if (input.name != null && !input.name.equals(id.get())) {
throw new BadRequestException("name in input must match name in URL");
}
-
- try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
-
- LabelType labelType = createLabel(config, id.get(), input);
-
- if (input.commitMessage != null) {
- md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
- } else {
- md.setMessage("Update label");
- }
-
- config.commit(md);
-
- projectCache.evictAndReindex(rsrc.getProjectState().getProject());
-
+ try (var configUpdater =
+ repoMetaDataUpdater.configUpdater(rsrc.getNameKey(), input.commitMessage, "Update label")) {
+ LabelType labelType = createLabel(configUpdater.getConfig(), id.get(), input);
+ configUpdater.commitConfigUpdate();
return Response.created(LabelDefinitionJson.format(rsrc.getNameKey(), labelType));
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
index a46211c..b946958 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
@@ -26,12 +26,7 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-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.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.SubmitRequirementExpressionsValidator;
@@ -39,7 +34,6 @@
import com.google.gerrit.server.project.SubmitRequirementResource;
import com.google.gerrit.server.project.SubmitRequirementsUtil;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
@@ -50,42 +44,42 @@
public class CreateSubmitRequirement
implements RestCollectionCreateView<
ProjectResource, SubmitRequirementResource, SubmitRequirementInput> {
- private final Provider<CurrentUser> user;
- private final PermissionBackend permissionBackend;
- private final MetaDataUpdate.User updateFactory;
- private final ProjectConfig.Factory projectConfigFactory;
- private final ProjectCache projectCache;
private final SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator;
+ private final RepoMetaDataUpdater updater;
@Inject
public CreateSubmitRequirement(
- Provider<CurrentUser> user,
- PermissionBackend permissionBackend,
- MetaDataUpdate.User updateFactory,
- ProjectConfig.Factory projectConfigFactory,
- ProjectCache projectCache,
- SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator) {
- this.user = user;
- this.permissionBackend = permissionBackend;
- this.updateFactory = updateFactory;
- this.projectConfigFactory = projectConfigFactory;
- this.projectCache = projectCache;
+ SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator,
+ RepoMetaDataUpdater updater) {
this.submitRequirementExpressionsValidator = submitRequirementExpressionsValidator;
+ this.updater = updater;
}
@Override
public Response<SubmitRequirementInfo> apply(
ProjectResource rsrc, IdString id, SubmitRequirementInput input)
throws AuthException, BadRequestException, IOException, PermissionBackendException {
- if (!user.get().isIdentifiedUser()) {
- throw new AuthException("Authentication required");
+ String defaultMessage = String.format("Create Submit Requirement %s", id.get());
+ try (var configUpdater =
+ updater.configUpdater(
+ rsrc.getNameKey(),
+ /** message= */
+ null,
+ defaultMessage)) {
+ ProjectConfig config = configUpdater.getConfig();
+ SubmitRequirement submitRequirement = updateConfig(config, id, input);
+
+ configUpdater.commitConfigUpdate();
+ return Response.created(SubmitRequirementJson.format(submitRequirement));
+ } catch (ConfigInvalidException e) {
+ throw new IOException("Failed to read project config", e);
+ } catch (ResourceConflictException e) {
+ throw new BadRequestException("Failed to create submit requirement", e);
}
+ }
- permissionBackend
- .currentUser()
- .project(rsrc.getNameKey())
- .check(ProjectPermission.WRITE_CONFIG);
-
+ SubmitRequirement updateConfig(ProjectConfig config, IdString id, SubmitRequirementInput input)
+ throws ResourceConflictException, BadRequestException {
if (input == null) {
input = new SubmitRequirementInput();
}
@@ -93,23 +87,7 @@
if (input.name != null && !input.name.equals(id.get())) {
throw new BadRequestException("name in input must match name in URL");
}
-
- try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
-
- SubmitRequirement submitRequirement = createSubmitRequirement(config, id.get(), input);
-
- md.setMessage(String.format("Create Submit Requirement %s", submitRequirement.name()));
- config.commit(md);
-
- projectCache.evict(rsrc.getProjectState().getProject().getNameKey());
-
- return Response.created(SubmitRequirementJson.format(submitRequirement));
- } catch (ConfigInvalidException e) {
- throw new IOException("Failed to read project config", e);
- } catch (ResourceConflictException e) {
- throw new BadRequestException("Failed to create submit requirement", e);
- }
+ return createSubmitRequirement(config, id.get(), input);
}
public SubmitRequirement createSubmitRequirement(
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
index 8a1927a..fcd72a3 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
@@ -14,84 +14,49 @@
package com.google.gerrit.server.restapi.project;
-import com.google.common.base.Strings;
import com.google.gerrit.extensions.common.InputWithCommitMessage;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-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.project.LabelResource;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
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 DeleteLabel implements RestModifyView<LabelResource, InputWithCommitMessage> {
- private final Provider<CurrentUser> user;
- private final PermissionBackend permissionBackend;
- private final MetaDataUpdate.User updateFactory;
- private final ProjectConfig.Factory projectConfigFactory;
- private final ProjectCache projectCache;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
@Inject
- public DeleteLabel(
- Provider<CurrentUser> user,
- PermissionBackend permissionBackend,
- MetaDataUpdate.User updateFactory,
- ProjectConfig.Factory projectConfigFactory,
- ProjectCache projectCache) {
- this.user = user;
- this.permissionBackend = permissionBackend;
- this.updateFactory = updateFactory;
- this.projectConfigFactory = projectConfigFactory;
- this.projectCache = projectCache;
+ public DeleteLabel(RepoMetaDataUpdater repoMetaDataUpdater) {
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
public Response<?> apply(LabelResource rsrc, InputWithCommitMessage input)
throws AuthException, ResourceNotFoundException, PermissionBackendException, IOException,
- ConfigInvalidException {
- if (!user.get().isIdentifiedUser()) {
- throw new AuthException("Authentication required");
- }
-
- permissionBackend
- .currentUser()
- .project(rsrc.getProject().getNameKey())
- .check(ProjectPermission.WRITE_CONFIG);
-
+ ConfigInvalidException, BadRequestException {
if (input == null) {
input = new InputWithCommitMessage();
}
- try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
+ try (var configUpdater =
+ repoMetaDataUpdater.configUpdater(
+ rsrc.getProject().getNameKey(), input.commitMessage, "Delete label")) {
+ ProjectConfig config = configUpdater.getConfig();
if (!deleteLabel(config, rsrc.getLabelType().getName())) {
throw new ResourceNotFoundException(IdString.fromDecoded(rsrc.getLabelType().getName()));
}
-
- if (input.commitMessage != null) {
- md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
- } else {
- md.setMessage("Delete label");
- }
-
- config.commit(md);
+ configUpdater.commitConfigUpdate();
}
- projectCache.evictAndReindex(rsrc.getProject().getProjectState().getProject());
-
return Response.none();
}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/DeleteSubmitRequirement.java
index 1be4a5f..64e2399 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteSubmitRequirement.java
@@ -15,57 +15,30 @@
package com.google.gerrit.server.restapi.project;
import com.google.gerrit.extensions.common.Input;
-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.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.SubmitRequirementResource;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
@Singleton
public class DeleteSubmitRequirement implements RestModifyView<SubmitRequirementResource, Input> {
- private final Provider<CurrentUser> user;
- private final PermissionBackend permissionBackend;
- private final MetaDataUpdate.User updateFactory;
- private final ProjectConfig.Factory projectConfigFactory;
- private final ProjectCache projectCache;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
@Inject
- public DeleteSubmitRequirement(
- Provider<CurrentUser> user,
- PermissionBackend permissionBackend,
- MetaDataUpdate.User updateFactory,
- ProjectConfig.Factory projectConfigFactory,
- ProjectCache projectCache) {
- this.user = user;
- this.permissionBackend = permissionBackend;
- this.updateFactory = updateFactory;
- this.projectConfigFactory = projectConfigFactory;
- this.projectCache = projectCache;
+ public DeleteSubmitRequirement(RepoMetaDataUpdater repoMetaDataUpdater) {
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
public Response<?> apply(SubmitRequirementResource rsrc, Input input) throws Exception {
- if (!user.get().isIdentifiedUser()) {
- throw new AuthException("Authentication required");
- }
-
- permissionBackend
- .currentUser()
- .project(rsrc.getProject().getNameKey())
- .check(ProjectPermission.WRITE_CONFIG);
-
- try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
+ try (var configUpdater =
+ repoMetaDataUpdater.configUpdater(
+ rsrc.getProject().getNameKey(), null, "Delete submit requirement")) {
+ ProjectConfig config = configUpdater.getConfig();
if (!deleteSubmitRequirement(config, rsrc.getSubmitRequirement().name())) {
// This code is unreachable because the exception is thrown when rsrc was parsed
@@ -75,12 +48,9 @@
IdString.fromDecoded(rsrc.getSubmitRequirement().name())));
}
- md.setMessage("Delete submit requirement");
- config.commit(md);
+ configUpdater.commitConfigUpdate();
}
- projectCache.evict(rsrc.getProject().getProjectState().getProject().getNameKey());
-
return Response.none();
}
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
index a7e7894..c7b905e1d 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
@@ -84,6 +84,7 @@
get(PROJECT_KIND, "config").to(GetConfig.class);
put(PROJECT_KIND, "config").to(PutConfig.class);
+ put(PROJECT_KIND, "config:review").to(PutConfigReview.class);
post(PROJECT_KIND, "create.change").to(CreateChange.class);
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index d5f61ce..a0f174d 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -34,6 +34,7 @@
import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
import com.google.gerrit.extensions.client.InheritableBoolean;
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.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -47,15 +48,14 @@
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.extensions.webui.UiActions;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
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.project.BooleanProjectConfigTransformations;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigUpdater;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -65,7 +65,6 @@
import java.util.Map;
import java.util.regex.Pattern;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
@Singleton
@@ -76,8 +75,6 @@
Pattern.compile("^[a-zA-Z0-9]+[a-zA-Z0-9-]*$");
private final boolean serverEnableSignedPush;
- private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
- private final ProjectCache projectCache;
private final ProjectState.Factory projectStateFactory;
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
private final PluginConfigFactory cfgFactory;
@@ -88,11 +85,11 @@
private final PermissionBackend permissionBackend;
private final ProjectConfig.Factory projectConfigFactory;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
+
@Inject
PutConfig(
@EnableSignedPush boolean serverEnableSignedPush,
- Provider<MetaDataUpdate.User> metaDataUpdateFactory,
- ProjectCache projectCache,
ProjectState.Factory projectStateFactory,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
PluginConfigFactory cfgFactory,
@@ -101,10 +98,9 @@
DynamicMap<RestView<ProjectResource>> views,
Provider<CurrentUser> user,
PermissionBackend permissionBackend,
- ProjectConfig.Factory projectConfigFactory) {
+ ProjectConfig.Factory projectConfigFactory,
+ RepoMetaDataUpdater repoMetaDataUpdater) {
this.serverEnableSignedPush = serverEnableSignedPush;
- this.metaDataUpdateFactory = metaDataUpdateFactory;
- this.projectCache = projectCache;
this.projectStateFactory = projectStateFactory;
this.pluginConfigEntries = pluginConfigEntries;
this.cfgFactory = cfgFactory;
@@ -114,6 +110,7 @@
this.user = user;
this.permissionBackend = permissionBackend;
this.projectConfigFactory = projectConfigFactory;
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
@@ -127,72 +124,74 @@
}
public ConfigInfo apply(ProjectState projectState, ConfigInput input)
- throws ResourceNotFoundException, BadRequestException, ResourceConflictException {
+ throws ResourceNotFoundException, BadRequestException, ResourceConflictException,
+ PermissionBackendException, AuthException {
Project.NameKey projectName = projectState.getNameKey();
if (input == null) {
throw new BadRequestException("config is required");
}
-
- try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
- ProjectConfig projectConfig = projectConfigFactory.read(md);
- projectConfig.updateProject(
- p -> {
- p.setDescription(Strings.emptyToNull(input.description));
- for (BooleanProjectConfig cfg : BooleanProjectConfig.values()) {
- InheritableBoolean val = BooleanProjectConfigTransformations.get(cfg, input);
- if (val != null) {
- p.setBooleanConfig(cfg, val);
- }
- }
- if (input.maxObjectSizeLimit != null) {
- p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
- }
- if (input.submitType != null) {
- p.setSubmitType(input.submitType);
- }
- if (input.state != null) {
- p.setState(input.state);
- }
- });
-
- if (input.pluginConfigValues != null) {
- setPluginConfigValues(projectState, projectConfig, input.pluginConfigValues);
- }
-
- if (input.commentLinks != null) {
- updateCommentLinks(projectConfig, input.commentLinks);
- }
-
- md.setMessage("Modified project settings\n");
- try {
- projectConfig.commit(md);
- projectCache.evictAndReindex(projectConfig.getProject());
- md.getRepository().setGitwebDescription(projectConfig.getProject().getDescription());
- } catch (IOException e) {
- if (e.getCause() instanceof ConfigInvalidException) {
- throw new ResourceConflictException(
- "Cannot update " + projectName + ": " + e.getCause().getMessage());
- }
- logger.atWarning().withCause(e).log("Failed to update config of project %s.", projectName);
- throw new ResourceConflictException("Cannot update " + projectName);
- }
-
- ProjectState state = projectStateFactory.create(projectConfigFactory.read(md).getCacheable());
+ try (ConfigUpdater updater =
+ repoMetaDataUpdater.configUpdater(
+ projectName, input.commitMessage, "Modified project settings")) {
+ updateConfig(projectState, updater.getConfig(), input);
+ updater.commitConfigUpdate();
+ updater
+ .getRepository()
+ .setGitwebDescription(updater.getConfig().getProject().getDescription());
+ ProjectState newProjectState =
+ projectStateFactory.create(
+ projectConfigFactory.read(updater.getRepository(), projectName).getCacheable());
return ConfigInfoCreator.constructInfo(
serverEnableSignedPush,
- state,
+ newProjectState,
user.get(),
pluginConfigEntries,
cfgFactory,
allProjects,
uiActions,
views);
- } catch (RepositoryNotFoundException notFound) {
- throw new ResourceNotFoundException(projectName.get(), notFound);
+
+ } catch (IOException e) {
+ if (e.getCause() instanceof ConfigInvalidException) {
+ throw new ResourceConflictException(
+ "Cannot update " + projectName + ": " + e.getCause().getMessage());
+ }
+ logger.atWarning().withCause(e).log("Failed to update config of project %s.", projectName);
+ throw new ResourceConflictException("Cannot update " + projectName);
} catch (ConfigInvalidException err) {
throw new ResourceConflictException("Cannot read project " + projectName, err);
- } catch (IOException err) {
- throw new ResourceConflictException("Cannot update project " + projectName, err);
+ }
+ }
+
+ public void updateConfig(
+ ProjectState projectState, ProjectConfig projectConfig, ConfigInput input)
+ throws BadRequestException {
+ projectConfig.updateProject(
+ p -> {
+ p.setDescription(Strings.emptyToNull(input.description));
+ for (BooleanProjectConfig cfg : BooleanProjectConfig.values()) {
+ InheritableBoolean val = BooleanProjectConfigTransformations.get(cfg, input);
+ if (val != null) {
+ p.setBooleanConfig(cfg, val);
+ }
+ }
+ if (input.maxObjectSizeLimit != null) {
+ p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
+ }
+ if (input.submitType != null) {
+ p.setSubmitType(input.submitType);
+ }
+ if (input.state != null) {
+ p.setState(input.state);
+ }
+ });
+
+ if (input.pluginConfigValues != null) {
+ setPluginConfigValues(projectState, projectConfig, input.pluginConfigValues);
+ }
+
+ if (input.commentLinks != null) {
+ updateCommentLinks(projectConfig, input.commentLinks);
}
}
@@ -302,7 +301,7 @@
}
}
- private void updateCommentLinks(
+ private static void updateCommentLinks(
ProjectConfig projectConfig, Map<String, CommentLinkInput> input) {
for (Map.Entry<String, CommentLinkInput> e : input.entrySet()) {
String name = e.getKey();
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfigReview.java b/java/com/google/gerrit/server/restapi/project/PutConfigReview.java
new file mode 100644
index 0000000..5c51003
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/PutConfigReview.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2024 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.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+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.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigChangeCreator;
+import com.google.gerrit.server.update.UpdateException;
+import java.io.IOException;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class PutConfigReview implements RestModifyView<ProjectResource, ConfigInput> {
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
+ private final PutConfig putConfig;
+
+ @Inject
+ PutConfigReview(RepoMetaDataUpdater repoMetaDataUpdater, PutConfig putConfig) {
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
+ this.putConfig = putConfig;
+ }
+
+ @Override
+ public Response<ChangeInfo> apply(ProjectResource rsrc, ConfigInput input)
+ throws PermissionBackendException, IOException, ConfigInvalidException, UpdateException,
+ RestApiException {
+ try (ConfigChangeCreator creator =
+ repoMetaDataUpdater.configChangeCreator(
+ rsrc.getNameKey(), input.commitMessage, "Review config change")) {
+ putConfig.updateConfig(rsrc.getProjectState(), creator.getConfig(), input);
+ return creator.createChange();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/PutDescription.java b/java/com/google/gerrit/server/restapi/project/PutDescription.java
index ec42035..698cf58 100644
--- a/java/com/google/gerrit/server/restapi/project/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/project/PutDescription.java
@@ -14,24 +14,18 @@
package com.google.gerrit.server.restapi.project;
-import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.api.projects.DescriptionInput;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
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.RestModifyView;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-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.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
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;
@@ -39,53 +33,30 @@
@Singleton
public class PutDescription implements RestModifyView<ProjectResource, DescriptionInput> {
- private final ProjectCache cache;
- private final Provider<MetaDataUpdate.Server> updateFactory;
- private final PermissionBackend permissionBackend;
- private final ProjectConfig.Factory projectConfigFactory;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
@Inject
- PutDescription(
- ProjectCache cache,
- Provider<MetaDataUpdate.Server> updateFactory,
- PermissionBackend permissionBackend,
- ProjectConfig.Factory projectConfigFactory) {
- this.cache = cache;
- this.updateFactory = updateFactory;
- this.permissionBackend = permissionBackend;
- this.projectConfigFactory = projectConfigFactory;
+ PutDescription(RepoMetaDataUpdater repoMetaDataUpdater) {
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
public Response<String> apply(ProjectResource resource, DescriptionInput input)
throws AuthException, ResourceConflictException, ResourceNotFoundException, IOException,
- PermissionBackendException {
+ PermissionBackendException, BadRequestException {
if (input == null) {
input = new DescriptionInput(); // Delete would set description to null.
}
- IdentifiedUser user = resource.getUser().asIdentifiedUser();
- permissionBackend
- .user(user)
- .project(resource.getNameKey())
- .check(ProjectPermission.WRITE_CONFIG);
-
- try (MetaDataUpdate md = updateFactory.get().create(resource.getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
+ try (var configUpdater =
+ repoMetaDataUpdater.configUpdater(
+ resource.getNameKey(), input.commitMessage, "Update description")) {
+ ProjectConfig config = configUpdater.getConfig();
String desc = input.description;
config.updateProject(p -> p.setDescription(Strings.emptyToNull(desc)));
- String msg =
- MoreObjects.firstNonNull(
- Strings.emptyToNull(input.commitMessage), "Update description\n");
- if (!msg.endsWith("\n")) {
- msg += "\n";
- }
- md.setAuthor(user);
- md.setMessage(msg);
- config.commit(md);
- cache.evictAndReindex(resource.getProjectState().getProject());
- md.getRepository().setGitwebDescription(config.getProject().getDescription());
+ configUpdater.commitConfigUpdate();
+ configUpdater.getRepository().setGitwebDescription(config.getProject().getDescription());
return Strings.isNullOrEmpty(config.getProject().getDescription())
? Response.none()
diff --git a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
index c45a009..5aea2c5 100644
--- a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
+++ b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
@@ -15,24 +15,28 @@
package com.google.gerrit.server.restapi.project;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
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.ImmutableMap;
+import com.google.errorprone.annotations.MustBeClosed;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.InvalidNameException;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.CreateGroupPermissionSyncer;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.git.meta.MetaDataUpdate.User;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -49,18 +53,20 @@
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
+import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
/** Updates repo refs/meta/config content. */
@Singleton
public class RepoMetaDataUpdater {
- private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
private final Provider<User> metaDataUpdateFactory;
+ private final Provider<CurrentUser> user;
private final ProjectConfig.Factory projectConfigFactory;
private final ProjectCache projectCache;
private final ChangeInserter.Factory changeInserterFactory;
@@ -69,38 +75,61 @@
private final BatchUpdate.Factory updateFactory;
private final PermissionBackend permissionBackend;
+ private final ChangeJson.Factory jsonFactory;
@Inject
RepoMetaDataUpdater(
- CreateGroupPermissionSyncer createGroupPermissionSyncer,
Provider<User> metaDataUpdateFactory,
+ Provider<CurrentUser> user,
ProjectConfig.Factory projectConfigFactory,
ProjectCache projectCache,
ChangeInserter.Factory changeInserterFactory,
Sequences seq,
BatchUpdate.Factory updateFactory,
- PermissionBackend permissionBackend) {
- this.createGroupPermissionSyncer = createGroupPermissionSyncer;
+ PermissionBackend permissionBackend,
+ ChangeJson.Factory jsonFactory) {
this.metaDataUpdateFactory = metaDataUpdateFactory;
+ this.user = user;
this.projectConfigFactory = projectConfigFactory;
this.projectCache = projectCache;
this.changeInserterFactory = changeInserterFactory;
this.seq = seq;
this.updateFactory = updateFactory;
this.permissionBackend = permissionBackend;
+ this.jsonFactory = jsonFactory;
}
- public Change updateAndCreateChangeForReview(
- Project.NameKey projectName,
- CurrentUser user,
- String message,
- ProjectConfigUpdater projectConfigUpdater)
- throws ConfigInvalidException, IOException, RestApiException, UpdateException,
- InvalidNameException, PermissionBackendException {
- checkArgument(!message.isBlank(), "The message must not be empty");
- message = validateMessage(message);
-
- PermissionBackend.ForProject forProject = permissionBackend.user(user).project(projectName);
+ /**
+ * Returns a creator for creating project config changes.
+ *
+ * <p>The method checks that user has required permissions.
+ *
+ * <p>Usage:
+ *
+ * <pre>{@code
+ * try(var changeCreator =
+ * repoMetaDataUpdater.configChangeCreator(projectName, message, defaultMessage)) {
+ * ProjectConfig config = changeCreator.getConfig();
+ * // ... update project config
+ * // Create change - if the createChange method is not called, all updates are ignored and no
+ * // change is created.
+ * Response<ChangeInfo> result = changeCreator.createChange();
+ * }
+ * }</pre>
+ *
+ * @param projectName the name of the project whose config should be updated
+ * @param message the user-provided commit message. If it is not provided (i.e. it is null or
+ * empty) - the {@code defaultMessage} is used.
+ * @param defaultMessage the default commit message if the user doesn't provide one.
+ */
+ @MustBeClosed
+ public ConfigChangeCreator configChangeCreator(
+ Project.NameKey projectName, @Nullable String message, String defaultMessage)
+ throws PermissionBackendException, AuthException, ResourceConflictException, IOException,
+ ConfigInvalidException {
+ message = validateMessage(message, defaultMessage);
+ PermissionBackend.ForProject forProject =
+ permissionBackend.user(user.get()).project(projectName);
if (!check(forProject, ProjectPermission.READ_CONFIG)) {
throw new AuthException(RefNames.REFS_CONFIG + " not visible");
}
@@ -112,15 +141,153 @@
}
}
projectCache.get(projectName).orElseThrow(illegalState(projectName)).checkStatePermitsWrite();
-
- try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
- ProjectConfig config = projectConfigFactory.read(md);
- ObjectId oldCommit = config.getRevision();
- String oldCommitSha1 = oldCommit == null ? null : oldCommit.getName();
-
- projectConfigUpdater.update(config);
- md.setMessage(message);
+ // The MetaDataUpdate instance gets closed in the ConfigChangeCreator.close() method.
+ MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName);
+ try {
md.setInsertChangeId(true);
+ md.setMessage(message);
+ ProjectConfig config = projectConfigFactory.read(md);
+ return new ConfigChangeCreator(md, projectName, user.get(), config);
+ } catch (Throwable t) {
+ try (md) {
+ throw t;
+ }
+ }
+ }
+
+ /**
+ * Returns an updater for updating project config without review.
+ *
+ * <p>The method checks that user has required permissions.
+ *
+ * <p>When the update is saved (using the {@link ConfigUpdater#commitConfigUpdate} method), the
+ * project cache is updated automatically.
+ *
+ * <p>Usage:
+ *
+ * <pre>{@code
+ * try(var configUpdater =
+ * repoMetaDataUpdater.configUpdater(projectName, message, defaultMessage)) {
+ * ProjectConfig config = changeCreator.getConfig();
+ * // ... update project config
+ * // Save updated config - if the commitConfigUpdate method is not called, all updates are ignored.
+ * configUpdater.commitConfigUpdate();
+ * }
+ * }</pre>
+ *
+ * @param projectName the name of the project whose config should be updated
+ * @param message the user-provided commit message. If it is not provided (i.e. it is null or
+ * empty) - the {@code defaultMessage} is used.
+ * @param defaultMessage the default commit message if the user doesn't provide one.
+ */
+ @MustBeClosed
+ public ConfigUpdater configUpdater(
+ Project.NameKey projectName, @Nullable String message, String defaultMessage)
+ throws AuthException, PermissionBackendException, ConfigInvalidException, IOException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+ permissionBackend.user(user.get()).project(projectName).check(ProjectPermission.WRITE_CONFIG);
+ return configUpdaterWithoutPermissionsCheck(projectName, message, defaultMessage);
+ }
+
+ /**
+ * Returns an updater for updating project config without review and skips some permissions
+ * checks.
+ *
+ * <p>The method doesn't do any permissions checks. It should be used only when standard
+ * permissions checks from {@link #configUpdater} can't be used.
+ *
+ * <p>See {@link #configUpdater} for details.
+ */
+ @MustBeClosed
+ public ConfigUpdater configUpdaterWithoutPermissionsCheck(
+ Project.NameKey projectName, @Nullable String message, String defaultMessage)
+ throws IOException, ConfigInvalidException {
+ message = validateMessage(message, defaultMessage);
+ // The MetaDataUpdate instance gets closed in the ConfigUpdater.close() method.
+ MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName);
+ try {
+ ProjectConfig config = projectConfigFactory.read(md);
+ md.setMessage(message);
+ return new ConfigUpdater(md, config);
+ } catch (Throwable t) {
+ try (md) {
+ throw t;
+ }
+ }
+ }
+
+ /**
+ * Updater for a project config without review.
+ *
+ * <p>See {@link #configUpdater} and {@link #configUpdaterWithoutPermissionsCheck} for details and
+ * usages.
+ */
+ public class ConfigUpdater implements AutoCloseable {
+ private final MetaDataUpdate md;
+ private final ProjectConfig config;
+
+ private ConfigUpdater(MetaDataUpdate md, ProjectConfig config) {
+ this.md = md;
+ this.config = config;
+ }
+
+ public ProjectConfig getConfig() {
+ return config;
+ }
+
+ public void commitConfigUpdate() throws IOException {
+ config.commit(md);
+ projectCache.evictAndReindex(config.getProject());
+ }
+
+ public Repository getRepository() {
+ return md.getRepository();
+ }
+
+ @Override
+ public void close() {
+ md.close();
+ }
+ }
+
+ /**
+ * Creates a change for a project config update.
+ *
+ * <p>See {@link #createChange} for details and usages.
+ */
+ public class ConfigChangeCreator implements AutoCloseable {
+ private final MetaDataUpdate md;
+ private final String oldCommitSha1;
+ private final Project.NameKey projectName;
+ private final CurrentUser user;
+ private final ProjectConfig config;
+ private boolean changeCreated;
+
+ private ConfigChangeCreator(
+ MetaDataUpdate md, Project.NameKey projectName, CurrentUser user, ProjectConfig config) {
+ this.md = md;
+ this.config = config;
+ this.projectName = projectName;
+ this.user = user;
+ ObjectId oldCommit = config.getRevision();
+ oldCommitSha1 = oldCommit == null ? null : oldCommit.getName();
+ }
+
+ @Override
+ public void close() {
+ md.close();
+ }
+
+ public ProjectConfig getConfig() {
+ return config;
+ }
+
+ public Response<ChangeInfo> createChange()
+ throws IOException, UpdateException, RestApiException {
+ checkState(!changeCreated, "Change has been already created");
+ changeCreated = true;
Change.Id changeId = Change.id(seq.nextChangeId());
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
@@ -140,62 +307,38 @@
ChangeInserter ins = newInserter(changeId, commit);
bu.insertChange(ins);
bu.execute();
- return ins.getChange();
+ Change change = ins.getChange();
+ return Response.created(jsonFactory.noOptions().format(change));
}
}
}
- }
- public void updateWithoutReview(
- Project.NameKey projectName, String message, ProjectConfigUpdater projectConfigUpdater)
- throws ConfigInvalidException, IOException, PermissionBackendException, AuthException,
- ResourceConflictException, InvalidNameException, BadRequestException {
- updateWithoutReview(
- projectName, message, /*skipPermissionsCheck=*/ false, projectConfigUpdater);
- }
-
- public void updateWithoutReview(
- Project.NameKey projectName,
- String message,
- boolean skipPermissionsCheck,
- ProjectConfigUpdater projectConfigUpdater)
- throws ConfigInvalidException, IOException, PermissionBackendException, AuthException,
- ResourceConflictException, InvalidNameException, BadRequestException {
- message = validateMessage(message);
- if (!skipPermissionsCheck) {
- permissionBackend.currentUser().project(projectName).check(ProjectPermission.WRITE_CONFIG);
- }
-
- try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
- ProjectConfig config = projectConfigFactory.read(md);
-
- projectConfigUpdater.update(config);
- md.setMessage(message);
- config.commit(md);
- projectCache.evictAndReindex(config.getProject());
- createGroupPermissionSyncer.syncIfNeeded();
+ // ProjectConfig doesn't currently support fusing into a BatchUpdate.
+ @SuppressWarnings("deprecation")
+ private ChangeInserter newInserter(Change.Id changeId, RevCommit commit) {
+ return changeInserterFactory
+ .create(changeId, commit, RefNames.REFS_CONFIG)
+ .setMessage(
+ // Same message as in ReceiveCommits.CreateRequest.
+ ApprovalsUtil.renderMessageWithApprovals(1, ImmutableMap.of(), ImmutableMap.of()))
+ .setValidate(false)
+ .setUpdateRef(false);
}
}
- private String validateMessage(String message) {
+ private String validateMessage(@Nullable String message, String defaultMessage) {
+ if (Strings.isNullOrEmpty(message)) {
+ message = defaultMessage;
+ } else {
+ message = message.trim();
+ }
+ checkArgument(!message.isBlank(), "The message must not be empty");
if (!message.endsWith("\n")) {
return message + "\n";
}
return message;
}
- // ProjectConfig doesn't currently support fusing into a BatchUpdate.
- @SuppressWarnings("deprecation")
- private ChangeInserter newInserter(Change.Id changeId, RevCommit commit) {
- return changeInserterFactory
- .create(changeId, commit, RefNames.REFS_CONFIG)
- .setMessage(
- // Same message as in ReceiveCommits.CreateRequest.
- ApprovalsUtil.renderMessageWithApprovals(1, ImmutableMap.of(), ImmutableMap.of()))
- .setValidate(false)
- .setUpdateRef(false);
- }
-
private boolean check(PermissionBackend.ForProject perm, ProjectPermission p)
throws PermissionBackendException {
try {
@@ -205,11 +348,4 @@
return false;
}
}
-
- @FunctionalInterface
- public interface ProjectConfigUpdater {
- void update(ProjectConfig config)
- throws BadRequestException, InvalidNameException, PermissionBackendException,
- ResourceConflictException, AuthException;
- }
}
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccess.java b/java/com/google/gerrit/server/restapi/project/SetAccess.java
index 75fe280..65851c0 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccess.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.restapi.project;
-import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.entities.AccessSection;
@@ -29,12 +28,15 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CreateGroupPermissionSyncer;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.RepoMetaDataUpdater.ConfigUpdater;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -49,6 +51,7 @@
private final Provider<IdentifiedUser> identifiedUser;
private final SetAccessUtil accessUtil;
private final RepoMetaDataUpdater repoMetaDataUpdater;
+ private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
@Inject
private SetAccess(
@@ -57,6 +60,7 @@
GetAccess getAccess,
Provider<IdentifiedUser> identifiedUser,
SetAccessUtil accessUtil,
+ CreateGroupPermissionSyncer createGroupPermissionSyncer,
RepoMetaDataUpdater repoMetaDataUpdater) {
this.groupBackend = groupBackend;
this.permissionBackend = permissionBackend;
@@ -64,6 +68,7 @@
this.identifiedUser = identifiedUser;
this.accessUtil = accessUtil;
this.repoMetaDataUpdater = repoMetaDataUpdater;
+ this.createGroupPermissionSyncer = createGroupPermissionSyncer;
}
@Override
@@ -75,42 +80,40 @@
accessUtil.getAccessSections(input.remove, /* rejectNonResolvableGroups= */ false);
ImmutableList<AccessSection> additions =
accessUtil.getAccessSections(input.add, /* rejectNonResolvableGroups= */ true);
- String message = !Strings.isNullOrEmpty(input.message) ? input.message : "Modify access rules";
- try {
- this.repoMetaDataUpdater.updateWithoutReview(
+
+ try (ConfigUpdater updater =
+ repoMetaDataUpdater.configUpdaterWithoutPermissionsCheck(
+ rsrc.getNameKey(), input.message, "Modify access rules")) {
+ ProjectConfig config = updater.getConfig();
+ boolean checkedAdmin = false;
+ for (AccessSection section : Iterables.concat(additions, removals)) {
+ boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
+ if (isGlobalCapabilities) {
+ if (!checkedAdmin) {
+ permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+ checkedAdmin = true;
+ }
+ } else {
+ permissionBackend
+ .currentUser()
+ .project(rsrc.getNameKey())
+ .ref(section.getName())
+ .check(RefPermission.WRITE_CONFIG);
+ }
+ }
+
+ accessUtil.validateChanges(config, removals, additions);
+ accessUtil.applyChanges(config, removals, additions);
+
+ accessUtil.setParentName(
+ identifiedUser.get(),
+ config,
rsrc.getNameKey(),
- message,
- /*skipPermissionsCheck=*/ true,
- config -> {
- // Check that the user has the right permissions.
- boolean checkedAdmin = false;
- for (AccessSection section : Iterables.concat(additions, removals)) {
- boolean isGlobalCapabilities =
- AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
- if (isGlobalCapabilities) {
- if (!checkedAdmin) {
- permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
- checkedAdmin = true;
- }
- } else {
- permissionBackend
- .currentUser()
- .project(rsrc.getNameKey())
- .ref(section.getName())
- .check(RefPermission.WRITE_CONFIG);
- }
- }
+ input.parent == null ? null : Project.nameKey(input.parent),
+ !checkedAdmin);
- accessUtil.validateChanges(config, removals, additions);
- accessUtil.applyChanges(config, removals, additions);
-
- accessUtil.setParentName(
- identifiedUser.get(),
- config,
- rsrc.getNameKey(),
- input.parent == null ? null : Project.nameKey(input.parent),
- !checkedAdmin);
- });
+ updater.commitConfigUpdate();
+ createGroupPermissionSyncer.syncIfNeeded();
} catch (InvalidNameException e) {
throw new BadRequestException(e.toString());
} catch (ConfigInvalidException e) {
diff --git a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
index 853d7df..a46ee32 100644
--- a/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
+++ b/java/com/google/gerrit/server/restapi/project/SetDefaultDashboard.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.restapi.project;
-import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.api.projects.DashboardInfo;
import com.google.gerrit.extensions.api.projects.SetDashboardInput;
@@ -25,12 +24,8 @@
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.git.meta.MetaDataUpdate;
-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.project.DashboardResource;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectResource;
import com.google.inject.Inject;
@@ -41,30 +36,21 @@
import org.kohsuke.args4j.Option;
class SetDefaultDashboard implements RestModifyView<DashboardResource, SetDashboardInput> {
- private final ProjectCache cache;
- private final MetaDataUpdate.Server updateFactory;
private final DashboardsCollection dashboards;
private final Provider<GetDashboard> get;
- private final PermissionBackend permissionBackend;
- private final ProjectConfig.Factory projectConfigFactory;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
@Option(name = "--inherited", usage = "set dashboard inherited by children")
boolean inherited;
@Inject
SetDefaultDashboard(
- ProjectCache cache,
- MetaDataUpdate.Server updateFactory,
DashboardsCollection dashboards,
Provider<GetDashboard> get,
- PermissionBackend permissionBackend,
- ProjectConfig.Factory projectConfigFactory) {
- this.cache = cache;
- this.updateFactory = updateFactory;
+ RepoMetaDataUpdater repoMetaDataUpdater) {
this.dashboards = dashboards;
this.get = get;
- this.permissionBackend = permissionBackend;
- this.projectConfigFactory = projectConfigFactory;
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
@@ -75,11 +61,6 @@
}
input.id = Strings.emptyToNull(input.id);
- permissionBackend
- .user(rsrc.getUser())
- .project(rsrc.getProjectState().getNameKey())
- .check(ProjectPermission.WRITE_CONFIG);
-
DashboardResource target = null;
if (input.id != null) {
try {
@@ -93,29 +74,22 @@
throw new ResourceConflictException(e.getMessage());
}
}
+ String defaultMessage =
+ input.id == null
+ ? "Removed default dashboard.\n"
+ : String.format("Changed default dashboard to %s.\n", input.id);
- try (MetaDataUpdate md = updateFactory.create(rsrc.getProjectState().getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
+ try (var configUpdater =
+ repoMetaDataUpdater.configUpdater(
+ rsrc.getProjectState().getNameKey(), input.commitMessage, defaultMessage)) {
+ ProjectConfig config = configUpdater.getConfig();
String id = input.id;
if (inherited) {
config.updateProject(p -> p.setDefaultDashboard(id));
} else {
config.updateProject(p -> p.setLocalDefaultDashboard(id));
}
-
- String msg =
- MoreObjects.firstNonNull(
- Strings.emptyToNull(input.commitMessage),
- input.id == null
- ? "Removed default dashboard.\n"
- : String.format("Changed default dashboard to %s.\n", input.id));
- if (!msg.endsWith("\n")) {
- msg += "\n";
- }
- md.setAuthor(rsrc.getUser().asIdentifiedUser());
- md.setMessage(msg);
- config.commit(md);
- cache.evictAndReindex(rsrc.getProjectState().getProject());
+ configUpdater.commitConfigUpdate();
if (target != null) {
Response<DashboardInfo> response = get.get().apply(target);
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java
index edd165d..1553eda 100644
--- a/java/com/google/gerrit/server/restapi/project/SetLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -24,18 +24,12 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-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.project.LabelDefinitionJson;
import com.google.gerrit.server.project.LabelResource;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
@@ -43,63 +37,36 @@
@Singleton
public class SetLabel implements RestModifyView<LabelResource, LabelDefinitionInput> {
- private final Provider<CurrentUser> user;
- private final PermissionBackend permissionBackend;
- private final MetaDataUpdate.User updateFactory;
- private final ProjectConfig.Factory projectConfigFactory;
- private final ProjectCache projectCache;
private final ApprovalQueryBuilder approvalQueryBuilder;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
@Inject
public SetLabel(
- Provider<CurrentUser> user,
- PermissionBackend permissionBackend,
- MetaDataUpdate.User updateFactory,
- ProjectConfig.Factory projectConfigFactory,
- ProjectCache projectCache,
- ApprovalQueryBuilder approvalQueryBuilder) {
- this.user = user;
- this.permissionBackend = permissionBackend;
- this.updateFactory = updateFactory;
- this.projectConfigFactory = projectConfigFactory;
- this.projectCache = projectCache;
+ ApprovalQueryBuilder approvalQueryBuilder, RepoMetaDataUpdater repoMetaDataUpdater) {
this.approvalQueryBuilder = approvalQueryBuilder;
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
public Response<LabelDefinitionInfo> apply(LabelResource rsrc, LabelDefinitionInput input)
throws AuthException, BadRequestException, ResourceConflictException,
PermissionBackendException, IOException, ConfigInvalidException {
- if (!user.get().isIdentifiedUser()) {
- throw new AuthException("Authentication required");
- }
-
- permissionBackend
- .currentUser()
- .project(rsrc.getProject().getNameKey())
- .check(ProjectPermission.WRITE_CONFIG);
-
if (input == null) {
input = new LabelDefinitionInput();
}
LabelType labelType = rsrc.getLabelType();
- try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
+ try (var configUpdater =
+ repoMetaDataUpdater.configUpdater(
+ rsrc.getProject().getNameKey(), input.commitMessage, "Update label")) {
+ ProjectConfig config = configUpdater.getConfig();
if (updateLabel(config, labelType, input)) {
- if (input.commitMessage != null) {
- md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
- } else {
- md.setMessage("Update label");
- }
String newName = Strings.nullToEmpty(input.name).trim();
labelType =
config.getLabelSections().get(newName.isEmpty() ? labelType.getName() : newName);
-
- config.commit(md);
- projectCache.evictAndReindex(rsrc.getProject().getProjectState().getProject());
+ configUpdater.commitConfigUpdate();
}
}
return Response.ok(LabelDefinitionJson.format(rsrc.getProject().getNameKey(), labelType));
diff --git a/java/com/google/gerrit/server/restapi/project/SetParent.java b/java/com/google/gerrit/server/restapi/project/SetParent.java
index ef31dc5..ad8bc8a 100644
--- a/java/com/google/gerrit/server/restapi/project/SetParent.java
+++ b/java/com/google/gerrit/server/restapi/project/SetParent.java
@@ -38,7 +38,6 @@
import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
import com.google.gerrit.server.config.GerritConfigListener;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -48,7 +47,6 @@
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
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;
@@ -60,29 +58,26 @@
implements RestModifyView<ProjectResource, ParentInput>, GerritConfigListener {
private final ProjectCache cache;
private final PermissionBackend permissionBackend;
- private final Provider<MetaDataUpdate.Server> updateFactory;
private final AllProjectsName allProjects;
private final AllUsersName allUsers;
- private final ProjectConfig.Factory projectConfigFactory;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
private volatile boolean allowProjectOwnersToChangeParent;
@Inject
SetParent(
ProjectCache cache,
PermissionBackend permissionBackend,
- Provider<MetaDataUpdate.Server> updateFactory,
AllProjectsName allProjects,
AllUsersName allUsers,
- ProjectConfig.Factory projectConfigFactory,
- @GerritServerConfig Config config) {
+ @GerritServerConfig Config config,
+ RepoMetaDataUpdater repoMetaDataUpdater) {
this.cache = cache;
this.permissionBackend = permissionBackend;
- this.updateFactory = updateFactory;
this.allProjects = allProjects;
this.allUsers = allUsers;
- this.projectConfigFactory = projectConfigFactory;
this.allowProjectOwnersToChangeParent =
config.getBoolean("receive", "allowProjectOwnersToChangeParent", false);
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
@@ -101,20 +96,14 @@
String parentName =
MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
validateParentUpdate(rsrc.getProjectState().getNameKey(), user, parentName, checkIfAdmin);
- try (MetaDataUpdate md = updateFactory.get().create(rsrc.getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
+ try (var configUpdater =
+ repoMetaDataUpdater.configUpdaterWithoutPermissionsCheck(
+ rsrc.getNameKey(),
+ input.commitMessage,
+ String.format("Changed parent to %s.\n", parentName))) {
+ ProjectConfig config = configUpdater.getConfig();
config.updateProject(p -> p.setParent(parentName));
-
- String msg = Strings.emptyToNull(input.commitMessage);
- if (msg == null) {
- msg = String.format("Changed parent to %s.\n", parentName);
- } else if (!msg.endsWith("\n")) {
- msg += "\n";
- }
- md.setAuthor(user);
- md.setMessage(msg);
- config.commit(md);
- cache.evictAndReindex(rsrc.getProjectState().getProject());
+ configUpdater.commitConfigUpdate();
Project.NameKey parent = config.getProject().getParent(allProjects);
requireNonNull(parent);
diff --git a/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
index 3e1104e..7eb2665 100644
--- a/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
+++ b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
@@ -24,19 +24,13 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-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.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.SubmitRequirementExpressionsValidator;
import com.google.gerrit.server.project.SubmitRequirementJson;
import com.google.gerrit.server.project.SubmitRequirementResource;
import com.google.gerrit.server.project.SubmitRequirementsUtil;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
@@ -48,42 +42,21 @@
@Singleton
public class UpdateSubmitRequirement
implements RestModifyView<SubmitRequirementResource, SubmitRequirementInput> {
- private final Provider<CurrentUser> user;
- private final PermissionBackend permissionBackend;
- private final MetaDataUpdate.User updateFactory;
- private final ProjectConfig.Factory projectConfigFactory;
- private final ProjectCache projectCache;
private final SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator;
+ private final RepoMetaDataUpdater repoMetaDataUpdater;
@Inject
public UpdateSubmitRequirement(
- Provider<CurrentUser> user,
- PermissionBackend permissionBackend,
- MetaDataUpdate.User updateFactory,
- ProjectConfig.Factory projectConfigFactory,
- ProjectCache projectCache,
- SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator) {
- this.user = user;
- this.permissionBackend = permissionBackend;
- this.updateFactory = updateFactory;
- this.projectConfigFactory = projectConfigFactory;
- this.projectCache = projectCache;
+ SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator,
+ RepoMetaDataUpdater repoMetaDataUpdater) {
this.submitRequirementExpressionsValidator = submitRequirementExpressionsValidator;
+ this.repoMetaDataUpdater = repoMetaDataUpdater;
}
@Override
public Response<SubmitRequirementInfo> apply(
SubmitRequirementResource rsrc, SubmitRequirementInput input)
throws AuthException, BadRequestException, PermissionBackendException, IOException {
- if (!user.get().isIdentifiedUser()) {
- throw new AuthException("Authentication required");
- }
-
- permissionBackend
- .currentUser()
- .project(rsrc.getProject().getNameKey())
- .check(ProjectPermission.WRITE_CONFIG);
-
if (input == null) {
input = new SubmitRequirementInput();
}
@@ -92,16 +65,17 @@
throw new BadRequestException("name in input must match name in URL");
}
- try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
- ProjectConfig config = projectConfigFactory.read(md);
+ try (var configUpdater =
+ repoMetaDataUpdater.configUpdater(
+ rsrc.getProject().getNameKey(),
+ null,
+ String.format("Update Submit Requirement %s", rsrc.getSubmitRequirement().name()))) {
+ ProjectConfig config = configUpdater.getConfig();
SubmitRequirement submitRequirement =
- createSubmitRequirement(config, rsrc.getSubmitRequirement().name(), input);
+ updateSubmitRequirement(config, rsrc.getSubmitRequirement().name(), input);
- md.setMessage(String.format("Update Submit Requirement %s", submitRequirement.name()));
- config.commit(md);
-
- projectCache.evict(rsrc.getProject().getNameKey());
+ configUpdater.commitConfigUpdate();
return Response.created(SubmitRequirementJson.format(submitRequirement));
} catch (ConfigInvalidException e) {
@@ -109,7 +83,7 @@
}
}
- public SubmitRequirement createSubmitRequirement(
+ public SubmitRequirement updateSubmitRequirement(
ProjectConfig config, String name, SubmitRequirementInput input) throws BadRequestException {
validateSRName(name);
if (Strings.isNullOrEmpty(input.submittabilityExpression)) {
diff --git a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
index cf3948a..fcd2264 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
@@ -54,6 +54,8 @@
// confusing and could stand to be reworked. Another smell is that this is an interface only for
// testing purposes.
public class SchemaCreatorImpl implements SchemaCreator {
+ public static final String BLOCKED_USERS = "Blocked Users";
+
private final GitRepositoryManager repoManager;
private final AllProjectsCreator allProjectsCreator;
private final AllUsersCreator allUsersCreator;
@@ -92,7 +94,7 @@
try (RefUpdateContext ctx = RefUpdateContext.open(RefUpdateType.INIT_REPO)) {
GroupReference admins = createGroupReference("Administrators");
GroupReference serviceUsers = createGroupReference(ServiceUserClassifier.SERVICE_USERS);
- GroupReference blockedUsers = createGroupReference("Blocked Users");
+ GroupReference blockedUsers = createGroupReference(BLOCKED_USERS);
AllProjectsInput allProjectsInput =
AllProjectsInput.builder()
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ConfigReviewIT.java b/javatests/com/google/gerrit/acceptance/api/project/ConfigReviewIT.java
new file mode 100644
index 0000000..694cfc9
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/project/ConfigReviewIT.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2024 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.api.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.inject.Inject;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ConfigReviewIT extends AbstractDaemonTest {
+ @Inject private ProjectOperations projectOperations;
+
+ private Project.NameKey defaultMessageProject;
+ private Project.NameKey customMessageProject;
+
+ @Before
+ public void setUp() throws Exception {
+ defaultMessageProject = projectOperations.newProject().create();
+ customMessageProject = projectOperations.newProject().create();
+ }
+
+ @Test
+ public void createConfigChangeWithDefaultMessage() throws Exception {
+ ConfigInput in = new ConfigInput();
+ in.description = "Test project description";
+
+ ChangeInfo changeInfo = gApi.projects().name(defaultMessageProject.get()).configReview(in);
+
+ assertThat(changeInfo.subject).isEqualTo("Review config change");
+ Config config = new Config();
+ config.fromText(
+ gApi.changes()
+ .id(changeInfo.changeId)
+ .revision(1)
+ .file("project.config")
+ .content()
+ .asString());
+ assertThat(config.getString("project", null, "description"))
+ .isEqualTo("Test project description");
+ }
+
+ @Test
+ public void createConfigChangeWithCustomMessage() throws Exception {
+ ConfigInput in = new ConfigInput();
+ in.description = "Test project description";
+ String customMessage = "test custom message";
+ in.commitMessage = customMessage;
+
+ ChangeInfo changeInfo = gApi.projects().name(customMessageProject.get()).configReview(in);
+
+ assertThat(changeInfo.subject).isEqualTo(customMessage);
+ Config config = new Config();
+ config.fromText(
+ gApi.changes()
+ .id(changeInfo.changeId)
+ .revision(1)
+ .file("project.config")
+ .content()
+ .asString());
+ assertThat(config.getString("project", null, "description"))
+ .isEqualTo("Test project description");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
index b9cbbcd..2ce8f78 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/DashboardIT.java
@@ -25,6 +25,7 @@
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.DashboardInfo;
import com.google.gerrit.extensions.api.projects.DashboardSectionInfo;
@@ -102,6 +103,10 @@
DashboardInfo info = createTestDashboard();
assertThat(info.isDefault).isNull();
project().dashboard(info.id).setDefault();
+ assertLastCommitAuthorAndShortMessage(
+ RefNames.REFS_CONFIG,
+ "Administrator",
+ String.format("Changed default dashboard to %s.", info.id));
assertThat(project().dashboard(info.id).get().isDefault).isTrue();
assertThat(project().defaultDashboard().get().id).isEqualTo(info.id);
}
@@ -111,10 +116,16 @@
DashboardInfo info = createTestDashboard();
assertThat(info.isDefault).isNull();
project().defaultDashboard(info.id);
+ assertLastCommitAuthorAndShortMessage(
+ RefNames.REFS_CONFIG,
+ "Administrator",
+ String.format("Changed default dashboard to %s.", info.id));
assertThat(project().dashboard(info.id).get().isDefault).isTrue();
assertThat(project().defaultDashboard().get().id).isEqualTo(info.id);
project().removeDefaultDashboard();
+ assertLastCommitAuthorAndShortMessage(
+ RefNames.REFS_CONFIG, "Administrator", "Removed default dashboard.");
assertThat(project().dashboard(info.id).get().isDefault).isNull();
assertThrows(ResourceNotFoundException.class, () -> project().defaultDashboard().get());
diff --git a/javatests/com/google/gerrit/acceptance/api/project/PutDescriptionIT.java b/javatests/com/google/gerrit/acceptance/api/project/PutDescriptionIT.java
new file mode 100644
index 0000000..befb311
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/project/PutDescriptionIT.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2024 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.api.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.projects.DescriptionInput;
+import org.junit.Test;
+
+public class PutDescriptionIT extends AbstractDaemonTest {
+ @Test
+ public void setDescription() throws Exception {
+ DescriptionInput input = new DescriptionInput();
+ input.description = "test project description";
+ gApi.projects().name(project.get()).description(input);
+ assertThat(gApi.projects().name(project.get()).description())
+ .isEqualTo("test project description");
+ assertLastCommitAuthorAndShortMessage(
+ RefNames.REFS_CONFIG, "Administrator", "Update description");
+ }
+
+ @Test
+ public void setDescriptionWithCustomCommitMessage() throws Exception {
+ DescriptionInput input = new DescriptionInput();
+ input.description = "test project description with test commit message";
+ input.commitMessage = "test commit message";
+ gApi.projects().name(project.get()).description(input);
+ assertThat(gApi.projects().name(project.get()).description())
+ .isEqualTo("test project description with test commit message");
+ assertLastCommitAuthorAndShortMessage(
+ RefNames.REFS_CONFIG, "Administrator", "test commit message");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
index 2bdbe50..a839b6a 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/SetParentIT.java
@@ -25,6 +25,7 @@
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -89,12 +90,16 @@
gApi.projects().name(project.get()).parent(parent);
assertThat(gApi.projects().name(project.get()).parent()).isEqualTo(parent);
+ assertLastCommitAuthorAndShortMessage(
+ RefNames.REFS_CONFIG, "Administrator", String.format("Changed parent to %s.", parent));
// When the parent name is not explicitly set, it should be
// set to "All-Projects".
gApi.projects().name(project.get()).parent(null);
assertThat(gApi.projects().name(project.get()).parent())
.isEqualTo(AllProjectsNameProvider.DEFAULT);
+ assertLastCommitAuthorAndShortMessage(
+ RefNames.REFS_CONFIG, "Administrator", "Changed parent to All-Projects.");
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index c18cd55..c9f469e 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -57,6 +57,7 @@
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.schema.SchemaCreatorImpl;
import com.google.gerrit.testing.ConfigSuite;
import com.google.inject.Inject;
import java.util.ArrayList;
@@ -120,7 +121,7 @@
public void setUp() throws Exception {
admins = adminGroupUuid();
serviceUsers = groupUuid(ServiceUserClassifier.SERVICE_USERS);
- blockedUsers = groupUuid("Blocked Users");
+ blockedUsers = groupUuid(SchemaCreatorImpl.BLOCKED_USERS);
setUpPermissions();
setUpChanges();
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index db9c1e7..f00804d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -70,6 +70,7 @@
RestCall.get("/projects/%s/commits:in"),
RestCall.get("/projects/%s/config"),
RestCall.put("/projects/%s/config"),
+ RestCall.put("/projects/%s/config:review"),
RestCall.post("/projects/%s/create.change"),
RestCall.get("/projects/%s/dashboards"),
RestCall.get("/projects/%s/description"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java b/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
index e58757b..00d8f4e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
@@ -20,6 +20,7 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.server.account.ServiceUserClassifier;
+import com.google.gerrit.server.schema.SchemaCreatorImpl;
import com.google.gson.reflect.TypeToken;
import java.util.Map;
import org.junit.Test;
@@ -34,6 +35,7 @@
newGson()
.fromJson(response.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
assertThat(groupMap.keySet())
- .containsExactly("Administrators", "Blocked Users", ServiceUserClassifier.SERVICE_USERS);
+ .containsExactly(
+ "Administrators", SchemaCreatorImpl.BLOCKED_USERS, ServiceUserClassifier.SERVICE_USERS);
}
}
diff --git a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
index 3fae3ad..089ceea 100644
--- a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
+++ b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
@@ -91,7 +91,7 @@
GroupReference adminsGroup = createGroupReference("Administrators");
GroupReference serviceUsersGroup = createGroupReference(ServiceUserClassifier.SERVICE_USERS);
- GroupReference blockedUsersGroup = createGroupReference("Blocked Users");
+ GroupReference blockedUsersGroup = createGroupReference(SchemaCreatorImpl.BLOCKED_USERS);
AllProjectsInput allProjectsInput =
AllProjectsInput.builder()
.administratorsGroup(adminsGroup)
@@ -165,7 +165,7 @@
public void createAllProjectsWithoutInitializingDefaultSubmitRequirements() throws Exception {
GroupReference adminsGroup = createGroupReference("Administrators");
GroupReference serviceUsersGroup = createGroupReference(ServiceUserClassifier.SERVICE_USERS);
- GroupReference blockedUsersGroup = createGroupReference("Blocked Users");
+ GroupReference blockedUsersGroup = createGroupReference(SchemaCreatorImpl.BLOCKED_USERS);
AllProjectsInput allProjectsInput =
AllProjectsInput.builder()
.administratorsGroup(adminsGroup)
diff --git a/lib/highlightjs/BUILD b/lib/highlightjs/BUILD
index 4105d85..d55a273 100644
--- a/lib/highlightjs/BUILD
+++ b/lib/highlightjs/BUILD
@@ -12,6 +12,7 @@
srcs = [
"@ui_npm//highlight.js",
"@ui_npm//highlightjs-closure-templates",
+ "@ui_npm//highlightjs-epp",
"@ui_npm//highlightjs-structured-text",
],
config_file = "rollup.config.js",
diff --git a/lib/highlightjs/index.js b/lib/highlightjs/index.js
index c2d048d..811dec7 100644
--- a/lib/highlightjs/index.js
+++ b/lib/highlightjs/index.js
@@ -17,9 +17,11 @@
import hljs from 'highlight.js';
import soy from 'highlightjs-closure-templates';
+import epp from 'highlightjs-epp';
import iecst from 'highlightjs-structured-text';
hljs.registerLanguage('soy', soy);
+hljs.registerLanguage('epp', epp);
hljs.registerLanguage('iecst', iecst);
export default hljs;
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 50ec339..836d86a 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
@@ -485,7 +485,7 @@
@state() pluginsLoaded = false;
- @state() threadsWithSuggestions?: CommentThread[];
+ @state() threadsWithUnappliedSuggestions?: CommentThread[];
private readonly restApiService = getAppContext().restApiService;
@@ -579,8 +579,8 @@
);
subscribe(
this,
- () => this.getCommentsModel().threadsWithSuggestions$,
- x => (this.threadsWithSuggestions = x)
+ () => this.getCommentsModel().threadsWithUnappliedSuggestions$,
+ x => (this.threadsWithUnappliedSuggestions = x)
);
}
@@ -820,11 +820,11 @@
<div class="header" slot="header">Publish Change Edit</div>
<div class="main" slot="main">
${when(
- this.numberOfThreadsWithSuggestions() > 0,
+ this.numberOfThreadsWithUnappliedSuggestions() > 0,
() => html`<p class="info">
<gr-icon id="icon" icon="info" small></gr-icon>
- Heads Up! ${this.numberOfThreadsWithSuggestions()} comments have
- suggestions you can apply before publishing
+ Heads Up! ${this.numberOfThreadsWithUnappliedSuggestions()}
+ comments have suggestions you can apply before publishing
</p>`
)}
Do you really want to publish the edit?
@@ -2105,8 +2105,16 @@
}
private handlePublishEditTap() {
- assertIsDefined(this.confirmPublishEditDialog, 'confirmPublishEditDialog');
- this.showActionDialog(this.confirmPublishEditDialog);
+ if (this.numberOfThreadsWithUnappliedSuggestions() > 0) {
+ assertIsDefined(
+ this.confirmPublishEditDialog,
+ 'confirmPublishEditDialog'
+ );
+ this.showActionDialog(this.confirmPublishEditDialog);
+ } else {
+ // Skip confirmation dialog and publish immediately.
+ this.handlePublishEditConfirm();
+ }
}
private handleRebaseEditTap() {
@@ -2264,9 +2272,9 @@
fireNoBubbleNoCompose(this, 'stop-edit-tap', {});
}
- private numberOfThreadsWithSuggestions() {
- if (!this.threadsWithSuggestions) return 0;
- return this.threadsWithSuggestions.length;
+ private numberOfThreadsWithUnappliedSuggestions() {
+ if (!this.threadsWithUnappliedSuggestions) return 0;
+ return this.threadsWithUnappliedSuggestions.length;
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents.ts b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents.ts
index 9cf5423..2dbbd26 100644
--- a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents.ts
+++ b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents.ts
@@ -3,6 +3,8 @@
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import '../gr-commit-info/gr-commit-info';
+import '../../shared/gr-button/gr-button';
import {customElement, state} from 'lit/decorators.js';
import {css, html, HTMLTemplateResult, LitElement} from 'lit';
import {resolve} from '../../../models/dependency';
diff --git a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts
index b9aa63d..d3b46ff 100644
--- a/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-revision-parents/gr-revision-parents_test.ts
@@ -167,7 +167,7 @@
The diff below may not be meaningful and may<br/>
even be hiding relevant changes.
<a href="/Documentation/user-review-ui.html#hazardous-rebases">Learn more</a>
- </p><p><gr-button link="">Show details</gr-button></p></div></div>`
+ </p><p><gr-button aria-disabled="false" link="" role="button" tabindex="0">Show details</gr-button></p></div></div>`
);
});
@@ -183,7 +183,7 @@
The diff below may not be meaningful and may<br/>
even be hiding relevant changes.
<a href="/Documentation/user-review-ui.html#hazardous-rebases">Learn more</a>
- </p><p><gr-button link="">Show details</gr-button></p></div></div>`
+ </p><p><gr-button aria-disabled="false" link="" role="button" tabindex="0">Show details</gr-button></p></div></div>`
);
});
@@ -235,7 +235,7 @@
The diff below may not be meaningful and may<br/>
even be hiding relevant changes.
<a href="/Documentation/user-review-ui.html#hazardous-rebases">Learn more</a>
- </p><p><gr-button link="">Show details</gr-button></p></div></div>`
+ </p><p><gr-button aria-disabled="false" link="" role="button" tabindex="0">Show details</gr-button></p></div></div>`
);
});
});
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
index e63ac8f..644871a4 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
@@ -32,6 +32,9 @@
@property({type: Object})
account?: AccountInfo;
+ @property({type: Boolean})
+ showMobile?: boolean;
+
// Private but used in test
@state()
config?: ServerInfo;
@@ -45,6 +48,9 @@
@state()
private switchAccountUrl = '';
+ // private but used in test
+ @state() feedbackURL = '';
+
// Private but used in test
readonly getConfigModel = resolve(this, configModelToken);
@@ -56,6 +62,10 @@
cfg => {
this.config = cfg;
+ if (cfg?.gerrit?.report_bug_url) {
+ this.feedbackURL = cfg?.gerrit.report_bug_url;
+ }
+
if (cfg && cfg.auth && cfg.auth.switch_account_url) {
this.switchAccountUrl = cfg.auth.switch_account_url;
} else {
@@ -103,7 +113,11 @@
@tap-item-shortcuts=${this.handleShortcutsTap}
.horizontalAlign=${'right'}
>
- <span ?hidden=${this.hasAvatars}>${this.accountName(this.account)}</span>
+ ${this.showMobile && !this.hasAvatars
+ ? html`<gr-icon icon="account_circle" filled></gr-icon>`
+ : html`<span ?hidden=${this.hasAvatars}
+ >${this.accountName(this.account)}</span
+ >`}
<gr-avatar
.account=${this.account}
?hidden=${!this.hasAvatars}
@@ -135,6 +149,15 @@
const url = this.interpolateUrl(switchAccountUrl, replacements);
links.push({name: 'Switch account', url, external: true});
}
+ if (this.showMobile && this.feedbackURL) {
+ links.push({
+ name: 'Feedback',
+ id: 'feedback',
+ url: this.feedbackURL,
+ external: true,
+ target: '_blank',
+ });
+ }
links.push({name: 'Sign out', id: 'signout', url: '/logout'});
return links;
}
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 ae59fb6..d133bca 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
@@ -22,13 +22,14 @@
import {getAppContext} from '../../../services/app-context';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, html, css} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
+import {customElement, property, query, state} from 'lit/decorators.js';
import {fire} from '../../../utils/event-util';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';
import {userModelToken} from '../../../models/user/user-model';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {subscribe} from '../../lit/subscription-controller';
+import {ifDefined} from 'lit/directives/if-defined.js';
type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;
@@ -104,6 +105,9 @@
AuthType.CUSTOM_EXTENSION,
]);
+const REL_NOOPENER = 'noopener';
+const REL_EXTERNAL = 'external';
+
declare global {
interface HTMLElementTagNameMap {
'gr-main-header': GrMainHeader;
@@ -149,6 +153,15 @@
// private but used in test
@state() feedbackURL = '';
+ @state() hamburgerClose? = false;
+
+ @query('.nav-sidebar') navSidebar?: HTMLDivElement;
+
+ @query('.modelBackground') modelBackground?: HTMLDivElement;
+
+ @query('.has-collapsible.active')
+ hasCollapsibleActive?: HTMLLIElement;
+
private readonly restApiService = getAppContext().restApiService;
private readonly getPluginLoader = resolve(this, pluginLoaderToken);
@@ -202,10 +215,29 @@
:host {
display: block;
}
- nav {
+ .hideOnDesktop {
+ display: none;
+ }
+
+ nav.hideOnMobile {
align-items: center;
display: flex;
}
+ nav.hideOnMobile ul {
+ list-style: none;
+ padding-left: var(--spacing-l);
+ }
+ nav.hideOnMobile .links > li {
+ cursor: default;
+ display: inline-block;
+ padding: 0;
+ position: relative;
+ }
+
+ .mobileTitle {
+ display: none;
+ }
+
.bigTitle {
color: var(--header-text-color);
font-size: var(--header-title-font-size);
@@ -215,7 +247,8 @@
.bigTitle:hover {
text-decoration: underline;
}
- .titleText {
+ .titleText,
+ .mobileTitleText {
/* Vertical alignment of icons and text with just block/inline display is too troublesome. */
display: flex;
align-items: center;
@@ -239,16 +272,45 @@
content: var(--header-title-content);
white-space: nowrap;
}
- ul {
- list-style: none;
- padding-left: var(--spacing-l);
+
+ .mobileTitleText::before {
+ --icon-width: var(
+ --header-icon-width,
+ var(--header-mobile-icon-size, var(--header-icon-size, 0))
+ );
+ --icon-height: var(
+ --header-icon-height,
+ var(--header-mobile-icon-size, var(--header-icon-size, 0))
+ );
+ background-image: var(--header-mobile-icon, var(--header-icon));
+ background-size: var(--mobile-icon-width, var(--icon-width))
+ var(--mobile-icon-height, var(--icon-height));
+ background-repeat: no-repeat;
+ content: '';
+ /* Any direct child of a flex element implicitly has 'display: block', but let's make that explicit here. */
+ display: block;
+ width: var(--mobile-icon-width, var(--icon-width));
+ height: var(--mobile-icon-height, var(--icon-height));
+ /* If size or height are set, then use 'spacing-m', 0px otherwise. */
+ margin-right: clamp(
+ 0px,
+ var(--mobile-icon-height, var(--icon-height)),
+ var(--spacing-m)
+ );
}
- .links > li {
- cursor: default;
- display: inline-block;
- padding: 0;
- position: relative;
+ .mobileTitleText::after {
+ /* The height will be determined by the line-height of the .bigTitle element. */
+ content: var(
+ --header-mobile-title-content,
+ var(--header-title-content)
+ );
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ flex: 1;
+ overflow: hidden;
+ min-width: 0;
}
+
.linksTitle {
display: inline-block;
font-weight: var(--font-weight-bold);
@@ -264,7 +326,24 @@
flex: 1;
justify-content: flex-end;
}
- .rightItems gr-endpoint-decorator:not(:empty) {
+ .mobileRightItems {
+ align-items: center;
+ justify-content: flex-end;
+
+ display: inline-block;
+ vertical-align: middle;
+ cursor: pointer;
+ position: relative;
+ top: 0px;
+ right: 0px;
+ margin-right: 0;
+ margin-left: auto;
+ min-height: 50px;
+ padding-top: 12px;
+ }
+
+ .rightItems gr-endpoint-decorator:not(:empty),
+ .mobileRightItems gr-endpoint-decorator:not(:empty) {
margin-left: var(--spacing-l);
}
gr-smart-search {
@@ -299,13 +378,17 @@
}
:host([loading]) .accountContainer,
:host([loggedIn]) .loginButton,
- :host([loggedIn]) .registerButton {
+ :host([loggedIn]) .registerButton,
+ :host([loggedIn]) .moreMenu {
display: none;
}
:host([loggedIn]) .settingsButton,
:host([loggedIn]) gr-account-dropdown {
display: inline;
}
+ :host:not([loggedIn]) .moreMenu {
+ display: inline;
+ }
.accountContainer {
flex: 0 0 auto;
align-items: center;
@@ -340,6 +423,10 @@
--gr-button-text-color: var(--header-text-color);
color: var(--header-text-color);
}
+ .hamburger-open {
+ --gr-button-text-color: var(--primary-text-color);
+ color: var(--primary-text-color);
+ }
#mobileSearch {
display: none;
}
@@ -363,7 +450,158 @@
margin-left: var(--spacing-m) !important;
}
gr-dropdown {
- padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-m);
+ padding: 0 var(--spacing-m);
+ }
+ .nav-sidebar {
+ background: var(--table-header-background-color);
+ width: 200px;
+ height: 100%;
+ display: block;
+ position: fixed;
+ left: -200px;
+ top: 0px;
+ transition: left 0.25s ease;
+ margin: 0;
+ border: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+ height: 100%;
+ margin-bottom: 15px 0;
+ box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
+ border-radius: 3px;
+ z-index: 2;
+ }
+ .nav-sidebar.visible {
+ left: 0px;
+ transition: left 0.25s ease;
+ width: 80%;
+ z-index: 200;
+ }
+ .mobileTitle {
+ position: relative;
+ display: block;
+ top: 10px;
+ font-size: 20px;
+ left: 100px;
+ right: 100px;
+ text-align: center;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ width: 50%;
+ }
+ .nav-header {
+ display: flex;
+ }
+ .hamburger {
+ display: inline-block;
+ vertical-align: middle;
+ height: 50px;
+ cursor: pointer;
+ margin: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
+ padding: 12px;
+ z-index: 200;
+ }
+ .nav-sidebar ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ display: block;
+ padding-top: 50px;
+ }
+ .nav-sidebar li {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ display: inline-block;
+ position: relative;
+ font-size: 14;
+ color: var(--primary-text-color);
+ display: block;
+ }
+ .cover {
+ background: rgba(0, 0, 0, 0.5);
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ overflow: none;
+ z-index: 199;
+ }
+ .hideOnDesktop {
+ display: block;
+ }
+ nav.hideOnMobile {
+ display: none;
+ }
+ .nav-sidebar .menu ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ display: block;
+ padding-top: 50px;
+ }
+ .nav-sidebar .menu li {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ display: inline-block;
+ position: relative;
+ font-size: 14;
+ color: var(--primary-text-color);
+ display: block;
+ }
+ .nav-sidebar .menu li a {
+ padding: 15px 20px;
+ font-size: 14;
+ outline: 0;
+ display: block;
+ color: var(--primary-text-color);
+ font-weight: 600;
+ }
+ .nav-sidebar .menu li.active ul.dropdown {
+ display: block;
+ }
+ .nav-sidebar .menu li ul.dropdown {
+ position: absolute;
+ display: none;
+ width: 100%;
+ box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
+ padding-top: 0;
+ position: relative;
+ }
+ .nav-sidebar .menu li ul.dropdown li {
+ display: block;
+ list-style-type: none;
+ }
+ .nav-sidebar .menu li ul.dropdown li a {
+ padding: 15px 20px;
+ font-size: 15px;
+ display: block;
+ font-weight: 400;
+ border-bottom: none;
+ padding: 10px 10px 10px 30px;
+ }
+ .nav-sidebar .menu li ul.dropdown li:last-child a {
+ border-bottom: none;
+ }
+ .nav-sidebar .menu a {
+ text-decoration: none;
+ }
+ .nav-sidebar .menu li.active:first-child a {
+ border-radius: 3px 0 0 3px;
+ border-radius: 0;
+ }
+ .nav-sidebar .menu li ul.dropdown li.active:first-child a {
+ border-radius: 0;
+ }
+ .arrow-down {
+ position: absolute;
+ top: 10px;
+ right: 10px;
}
}
`,
@@ -371,35 +609,134 @@
}
override render() {
+ return html` ${this.renderDesktop()} ${this.renderMobile()} `;
+ }
+
+ private renderDesktop() {
return html`
- <nav>
- <a href=${`//${window.location.host}${getBaseUrl()}/`} class="bigTitle">
- <gr-endpoint-decorator name="header-title">
- <div class="titleText"></div>
- </gr-endpoint-decorator>
- </a>
- <ul class="links">
- ${this.computeLinks(this.userLinks, this.adminLinks, this.topMenus).map(
- linkGroup => this.renderLinkGroup(linkGroup)
- )}
- </ul>
- <div class="rightItems">
- <gr-endpoint-decorator
- class="hideOnMobile"
- name="header-small-banner"
- ></gr-endpoint-decorator>
- <gr-smart-search id="search"></gr-smart-search>
- <gr-endpoint-decorator
- class="hideOnMobile"
- name="header-top-right"
- ></gr-endpoint-decorator>
- <gr-endpoint-decorator class="feedbackButton" name="header-feedback">
- ${this.renderFeedback()}
- </gr-endpoint-decorator>
- </div>
- ${this.renderAccount()}
- </div>
- </nav>
+ <nav class="hideOnMobile">
+ <a href=${`//${window.location.host}${getBaseUrl()}/`} class="bigTitle">
+ <gr-endpoint-decorator name="header-title">
+ <div class="titleText"></div>
+ </gr-endpoint-decorator>
+ </a>
+ <ul class="links">
+ ${this.computeLinks(
+ this.userLinks,
+ this.adminLinks,
+ this.topMenus
+ ).map(linkGroup => this.renderLinkGroup(linkGroup))}
+ </ul>
+ <div class="rightItems">
+ <gr-endpoint-decorator
+ class="hideOnMobile"
+ name="header-small-banner"
+ ></gr-endpoint-decorator>
+ <gr-smart-search id="search"></gr-smart-search>
+ <gr-endpoint-decorator
+ class="hideOnMobile"
+ name="header-top-right"
+ ></gr-endpoint-decorator>
+ <gr-endpoint-decorator class="feedbackButton" name="header-feedback">
+ ${this.renderFeedback()}
+ </gr-endpoint-decorator>
+ </div>
+ ${this.renderAccount()}
+ </div>
+ </nav>
+ `;
+ }
+
+ private renderMobile() {
+ const moreMenu: MainHeaderLink[] = [
+ {
+ name: this.registerText,
+ url: this.registerURL,
+ },
+ {
+ name: this.loginText,
+ url: this.loginUrl,
+ },
+ ];
+ if (!this.registerURL) {
+ moreMenu.shift();
+ }
+ if (this.feedbackURL) {
+ moreMenu.push({
+ name: 'Feedback',
+ url: this.feedbackURL,
+ external: true,
+ target: '_blank',
+ });
+ }
+
+ return html`
+ <nav class="hideOnDesktop">
+ <div class="nav-sidebar">
+ <ul class="menu">
+ ${this.computeLinks(
+ this.userLinks,
+ this.adminLinks,
+ this.topMenus
+ ).map(linkGroup => this.renderLinkGroupMobile(linkGroup))}
+ </ul>
+ </div>
+ <div class="nav-header">
+ <a
+ class="hamburger"
+ href=""
+ title="Hamburger"
+ aria-label="${!this.hamburgerClose ? 'Open' : 'Close'} hamburger"
+ role="button"
+ @click=${() => {
+ this.handleSidebar();
+ }}
+ >
+ ${!this.hamburgerClose
+ ? html`<gr-icon icon="menu" filled></gr-icon>`
+ : html`<gr-icon
+ class="hamburger-open"
+ icon="menu_open"
+ filled
+ ></gr-icon>`}
+ </a>
+ <a
+ href=${`//${window.location.host}${getBaseUrl()}/`}
+ class="mobileTitle bigTitle"
+ >
+ <gr-endpoint-decorator name="header-mobile-title">
+ <div class="mobileTitleText"></div>
+ </gr-endpoint-decorator>
+ </a>
+ <div class="mobileRightItems">
+ <a
+ class="searchButton"
+ title="Search"
+ @click=${(e: Event) => {
+ this.onMobileSearchTap(e);
+ }}
+ role="button"
+ aria-label=${this.mobileSearchHidden
+ ? 'Show Searchbar'
+ : 'Hide Searchbar'}
+ >
+ <gr-icon icon="search" filled></gr-icon>
+ </a>
+ <gr-dropdown
+ class="moreMenu"
+ link=""
+ .items=${moreMenu}
+ horizontal-align="center"
+ >
+ <span class="linksTitle">
+ <gr-icon icon="more_horiz" filled></gr-icon>
+ </span>
+ </gr-dropdown>
+ ${this.renderAccountDropdown(true)}
+ </div>
+ </div>
+ </nav>
+ <div class="modelBackground" @click=${() => this.handleSidebar()}></div>
`;
}
@@ -420,6 +757,41 @@
`;
}
+ private renderLinkGroupMobile(linkGroup: MainHeaderLinkGroup) {
+ return html`
+ <li class="has-collapsible" @click=${this.handleCollapsible}>
+ <a class="main" href="" data-title=${linkGroup.title}
+ >${linkGroup.title}<gr-icon
+ icon="arrow_drop_down"
+ class="arrow-down"
+ ></gr-icon
+ ></a>
+ <ul class="dropdown">
+ ${linkGroup.links.map(link => this.renderLinkMobile(link))}
+ </ul>
+ </li>
+ `;
+ }
+
+ private renderLinkMobile(link: DropdownLink) {
+ return html`
+ <li tabindex="-1">
+ <span ?hidden=${!!link.url} tabindex="-1">${link.name}</span>
+ <a
+ class="itemAction"
+ href=${this.computeLinkURL(link)}
+ ?download=${!!link.download}
+ rel=${ifDefined(this.computeLinkRel(link) ?? undefined)}
+ target=${ifDefined(link.target ?? undefined)}
+ ?hidden=${!link.url}
+ tabindex="-1"
+ @click=${() => this.handleSidebar()}
+ >${link.name}</a
+ >
+ </li>
+ `;
+ }
+
private renderFeedback() {
if (!this.feedbackURL) return;
@@ -483,11 +855,14 @@
`;
}
- private renderAccountDropdown() {
+ private renderAccountDropdown(showOnMobile?: boolean) {
if (!this.account) return;
return html`
- <gr-account-dropdown .account=${this.account}></gr-account-dropdown>
+ <gr-account-dropdown
+ .account=${this.account}
+ ?showMobile=${showOnMobile}
+ ></gr-account-dropdown>
`;
}
@@ -533,7 +908,6 @@
links.push({
title: 'Documentation',
links: docLinks,
- class: 'hideOnMobile',
});
}
links.push({
@@ -637,4 +1011,96 @@
e.stopPropagation();
fire(this, 'mobile-search', {});
}
+
+ /**
+ * Build a URL for the given host and path. The base URL will be only added,
+ * if it is not already included in the path.
+ *
+ * TODO: Move to util handler to remove duplication.
+ * @return The scheme-relative URL.
+ */
+ private computeURLHelper(host: string, path: string) {
+ const base = path.startsWith(getBaseUrl()) ? '' : getBaseUrl();
+ return '//' + host + base + path;
+ }
+
+ /**
+ * Build a scheme-relative URL for the current host. Will include the base
+ * URL if one is present. Note: the URL will be scheme-relative but absolute
+ * with regard to the host.
+ *
+ * TODO: Move to util handler to remove duplication.
+ * @param path The path for the URL.
+ * @return The scheme-relative URL.
+ */
+ private computeRelativeURL(path: string) {
+ const host = window.location.host;
+ return this.computeURLHelper(host, path);
+ }
+
+ /**
+ * Compute the URL for a link object.
+ *
+ * Private but used in tests.
+ *
+ * TODO: Move to util handler to remove duplication.
+ */
+ private computeLinkURL(link: DropdownLink) {
+ if (typeof link.url === 'undefined') {
+ return '';
+ }
+ if (link.target || !link.url.startsWith('/')) {
+ return link.url;
+ }
+ return this.computeRelativeURL(link.url);
+ }
+
+ /**
+ * Compute the value for the rel attribute of an anchor for the given link
+ * object. If the link has a target value, then the rel must be "noopener"
+ * for security reasons.
+ * Private but used in tests.
+ *
+ * TODO: Move to util handler to remove duplication.
+ */
+ private computeLinkRel(link: DropdownLink) {
+ // Note: noopener takes precedence over external.
+ if (link.target) {
+ return REL_NOOPENER;
+ }
+ if (link.external) {
+ return REL_EXTERNAL;
+ }
+ return null;
+ }
+
+ private handleCollapsible(e: MouseEvent) {
+ const target = e.target as HTMLSpanElement;
+ if (target.hasAttribute('data-title')) {
+ if (target.parentElement?.classList.contains('active')) {
+ target.parentElement.classList.remove('active');
+ } else {
+ if (this.hasCollapsibleActive) {
+ this.hasCollapsibleActive.classList.remove('active');
+ }
+ target.parentElement?.classList.toggle('active');
+ }
+ }
+ }
+
+ private handleSidebar() {
+ this.navSidebar?.classList.toggle('visible');
+ if (!this.modelBackground?.classList.contains('cover')) {
+ if (document.getElementsByTagName('html')) {
+ document.getElementsByTagName('html')[0].style.overflow = 'hidden';
+ }
+ } else {
+ if (document.getElementsByTagName('html')) {
+ document.getElementsByTagName('html')[0].style.overflow = '';
+ }
+ }
+ this.modelBackground?.classList.toggle('cover');
+ this.hasCollapsibleActive?.classList.remove('active');
+ this.hamburgerClose = !this.hamburgerClose;
+ }
}
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index 40430fb..b4a0600 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -39,7 +39,7 @@
assert.shadowDom.equal(
element,
/* HTML */ `
- <nav>
+ <nav class="hideOnMobile">
<a class="bigTitle" href="//localhost:9876/">
<gr-endpoint-decorator name="header-title">
<div class="titleText"></div>
@@ -51,7 +51,7 @@
<span class="linksTitle" id="Changes"> Changes </span>
</gr-dropdown>
</li>
- <li class="hideOnMobile">
+ <li>
<gr-dropdown down-arrow="" horizontal-align="left" link="">
<span class="linksTitle" id="Documentation">Documentation</span>
</gr-dropdown>
@@ -101,6 +101,169 @@
</a>
</div>
</nav>
+ <nav class="hideOnDesktop">
+ <div class="nav-sidebar">
+ <ul class="menu">
+ <li class="has-collapsible">
+ <a class="main" data-title="Changes" href="">
+ Changes
+ <gr-icon class="arrow-down" icon="arrow_drop_down"> </gr-icon>
+ </a>
+ <ul class="dropdown">
+ <li tabindex="-1">
+ <span hidden="" tabindex="-1"> Open </span>
+ <a
+ class="itemAction"
+ href="//localhost:9876/q/status:open+-is:wip"
+ tabindex="-1"
+ >
+ Open
+ </a>
+ </li>
+ <li tabindex="-1">
+ <span hidden="" tabindex="-1"> Merged </span>
+ <a
+ class="itemAction"
+ href="//localhost:9876/q/status:merged"
+ tabindex="-1"
+ >
+ Merged
+ </a>
+ </li>
+ <li tabindex="-1">
+ <span hidden="" tabindex="-1"> Abandoned </span>
+ <a
+ class="itemAction"
+ href="//localhost:9876/q/status:abandoned"
+ tabindex="-1"
+ >
+ Abandoned
+ </a>
+ </li>
+ </ul>
+ </li>
+ <li class="has-collapsible">
+ <a class="main" data-title="Documentation" href="">
+ Documentation
+ <gr-icon class="arrow-down" icon="arrow_drop_down"> </gr-icon>
+ </a>
+ <ul class="dropdown">
+ <li tabindex="-1">
+ <span hidden="" tabindex="-1"> Table of Contents </span>
+ <a
+ class="itemAction"
+ href="https://gerrit-review.googlesource.com/Documentation/index.html"
+ rel="noopener"
+ tabindex="-1"
+ target="_blank"
+ >
+ Table of Contents
+ </a>
+ </li>
+ <li tabindex="-1">
+ <span hidden="" tabindex="-1"> Searching </span>
+ <a
+ class="itemAction"
+ href="https://gerrit-review.googlesource.com/Documentation/user-search.html"
+ rel="noopener"
+ tabindex="-1"
+ target="_blank"
+ >
+ Searching
+ </a>
+ </li>
+ <li tabindex="-1">
+ <span hidden="" tabindex="-1"> Uploading </span>
+ <a
+ class="itemAction"
+ href="https://gerrit-review.googlesource.com/Documentation/user-upload.html"
+ rel="noopener"
+ tabindex="-1"
+ target="_blank"
+ >
+ Uploading
+ </a>
+ </li>
+ <li tabindex="-1">
+ <span hidden="" tabindex="-1"> Access Control </span>
+ <a
+ class="itemAction"
+ href="https://gerrit-review.googlesource.com/Documentation/access-control.html"
+ rel="noopener"
+ tabindex="-1"
+ target="_blank"
+ >
+ Access Control
+ </a>
+ </li>
+ <li tabindex="-1">
+ <span hidden="" tabindex="-1"> REST API </span>
+ <a
+ class="itemAction"
+ href="https://gerrit-review.googlesource.com/Documentation/rest-api.html"
+ rel="noopener"
+ tabindex="-1"
+ target="_blank"
+ >
+ REST API
+ </a>
+ </li>
+ <li tabindex="-1">
+ <span hidden="" tabindex="-1"> Project Owner Guide </span>
+ <a
+ class="itemAction"
+ href="https://gerrit-review.googlesource.com/Documentation/intro-project-owner.html"
+ rel="noopener"
+ tabindex="-1"
+ target="_blank"
+ >
+ Project Owner Guide
+ </a>
+ </li>
+ </ul>
+ </li>
+ <li class="has-collapsible">
+ <a class="main" data-title="Browse" href="">
+ Browse
+ <gr-icon class="arrow-down" icon="arrow_drop_down"> </gr-icon>
+ </a>
+ <ul class="dropdown"></ul>
+ </li>
+ </ul>
+ </div>
+ <div class="nav-header">
+ <a
+ aria-label="Open hamburger"
+ class="hamburger"
+ href=""
+ role="button"
+ title="Hamburger"
+ >
+ <gr-icon filled="" icon="menu"> </gr-icon>
+ </a>
+ <a class="bigTitle mobileTitle" href="//localhost:9876/">
+ <gr-endpoint-decorator name="header-mobile-title">
+ <div class="mobileTitleText"></div>
+ </gr-endpoint-decorator>
+ </a>
+ <div class="mobileRightItems">
+ <a
+ aria-label="Hide Searchbar"
+ class="searchButton"
+ role="button"
+ title="Search"
+ >
+ <gr-icon filled="" icon="search"> </gr-icon>
+ </a>
+ <gr-dropdown class="moreMenu" horizontal-align="center" link="">
+ <span class="linksTitle">
+ <gr-icon filled="" icon="more_horiz"> </gr-icon>
+ </span>
+ </gr-dropdown>
+ </div>
+ </div>
+ </nav>
+ <div class="modelBackground"></div>
`
);
});
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
index e5c2bc2..2220d2f 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
@@ -3,7 +3,6 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import '@polymer/iron-input/iron-input';
import '../../shared/gr-button/gr-button';
import {EmailInfo} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
@@ -14,6 +13,9 @@
import {ValueChangedEvent} from '../../../types/events';
import {fire} from '../../../utils/event-util';
import {deepClone} from '../../../utils/deep-util';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
+import {subscribe} from '../../lit/subscription-controller';
@customElement('gr-email-editor')
export class GrEmailEditor extends LitElement {
@@ -30,6 +32,21 @@
readonly restApiService = getAppContext().restApiService;
+ private readonly getUserModel = resolve(this, userModelToken);
+
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getUserModel().emails$,
+ x => {
+ if (!x) return;
+ this.originalEmails = deepClone<EmailInfo[]>(x);
+ this.emails = deepClone<EmailInfo[]>(x);
+ }
+ );
+ }
+
static override get styles() {
return [
sharedStyles,
@@ -83,24 +100,20 @@
return html`<tr>
<td class="emailColumn">${email.email}</td>
<td class="preferredControl" @click=${this.handlePreferredControlClick}>
- <iron-input
+ <!-- We have to use \`.checked\` rather then \`?checked\` as there
+ appears to be an issue when deleting, checked doesn't work correctly. -->
+ <input
class="preferredRadio"
+ type="radio"
+ name="preferred"
+ .value=${email.email}
+ .checked=${email.preferred}
@change=${this.handlePreferredChange}
- .bindValue=${email.email}
- >
- <input
- class="preferredRadio"
- type="radio"
- @change=${this.handlePreferredChange}
- name="preferred"
- ?checked=${email.preferred}
- />
- </iron-input>
+ />
</td>
<td>
<gr-button
- data-index=${index}
- @click=${this.handleDeleteButton}
+ @click=${() => this.handleDeleteButton(index)}
?disabled=${this.checkPreferred(email.preferred)}
class="remove-button"
>Delete</gr-button
@@ -109,14 +122,6 @@
</tr>`;
}
- loadData() {
- return this.restApiService.getAccountEmails().then(emails => {
- if (!emails) return;
- this.originalEmails = deepClone<EmailInfo[]>(emails);
- this.emails = emails;
- });
- }
-
save() {
const promises: Promise<unknown>[] = [];
@@ -130,26 +135,21 @@
);
}
- return Promise.all(promises).then(() => {
- this.originalEmails = this.emails;
+ return Promise.all(promises).then(async () => {
this.emailsToRemove = [];
this.newPreferred = '';
+ await this.getUserModel().loadEmails(true);
this.setHasUnsavedChanges();
});
}
- private handleDeleteButton(e: Event) {
- const target = e.target;
- if (!(target instanceof Element)) return;
- const indexStr = target.getAttribute('data-index');
- if (indexStr === null) return;
- const index = Number(indexStr);
+ private handleDeleteButton(index: number) {
const email = this.emails[index];
// Don't add project to emailsToRemove if it wasn't in
- // originalEmails.
+ // emails.
// We have to use JSON.stringify as we cloned the array
// so the reference is not the same.
- const emails = this.originalEmails.some(
+ const emails = this.emails.some(
x => JSON.stringify(email) === JSON.stringify(x)
);
if (emails) this.emailsToRemove.push(email);
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
index 39c3288..84ed8dc 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
@@ -11,6 +11,7 @@
suite('gr-email-editor tests', () => {
let element: GrEmailEditor;
+ let accountEmailStub: sinon.SinonStub;
setup(async () => {
const emails = [
@@ -19,13 +20,14 @@
{email: 'email@three.com'},
];
- stubRestApi('getAccountEmails').returns(Promise.resolve(emails));
+ accountEmailStub = stubRestApi('getAccountEmails').returns(
+ Promise.resolve(emails)
+ );
element = await fixture<GrEmailEditor>(
html`<gr-email-editor></gr-email-editor>`
);
- await element.loadData();
await element.updateComplete;
});
@@ -45,20 +47,17 @@
<tr>
<td class="emailColumn">email@one.com</td>
<td class="preferredControl">
- <iron-input class="preferredRadio">
- <input
- class="preferredRadio"
- name="preferred"
- type="radio"
- value="email@one.com"
- />
- </iron-input>
+ <input
+ class="preferredRadio"
+ name="preferred"
+ type="radio"
+ value="email@one.com"
+ />
</td>
<td>
<gr-button
aria-disabled="false"
class="remove-button"
- data-index="0"
role="button"
tabindex="0"
>
@@ -69,21 +68,17 @@
<tr>
<td class="emailColumn">email@two.com</td>
<td class="preferredControl">
- <iron-input class="preferredRadio">
- <input
- checked=""
- class="preferredRadio"
- name="preferred"
- type="radio"
- value="email@two.com"
- />
- </iron-input>
+ <input
+ class="preferredRadio"
+ name="preferred"
+ type="radio"
+ value="email@two.com"
+ />
</td>
<td>
<gr-button
aria-disabled="true"
class="remove-button"
- data-index="1"
disabled=""
role="button"
tabindex="-1"
@@ -95,20 +90,17 @@
<tr>
<td class="emailColumn">email@three.com</td>
<td class="preferredControl">
- <iron-input class="preferredRadio">
- <input
- class="preferredRadio"
- name="preferred"
- type="radio"
- value="email@three.com"
- />
- </iron-input>
+ <input
+ class="preferredRadio"
+ name="preferred"
+ type="radio"
+ value="email@three.com"
+ />
</td>
<td>
<gr-button
aria-disabled="false"
class="remove-button"
- data-index="2"
role="button"
tabindex="0"
>
@@ -239,6 +231,12 @@
assert.equal(element.emailsToRemove[0].email, 'email@one.com');
assert.equal(element.emails.length, 2);
+ accountEmailStub.restore();
+
+ accountEmailStub = stubRestApi('getAccountEmails').returns(
+ Promise.resolve(element.emails)
+ );
+
await element.save();
assert.equal(deleteEmailSpy.callCount, 1);
assert.equal(deleteEmailSpy.getCall(0).args[0], 'email@one.com');
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts
deleted file mode 100644
index c7118f7..0000000
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {LitElement, css, html} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-import {grFormStyles} from '../../../styles/gr-form-styles';
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-settings-item': GrSettingsItem;
- }
-}
-
-@customElement('gr-settings-item')
-export class GrSettingsItem extends LitElement {
- @property({type: String})
- anchor?: string;
-
- @property({type: String})
- override title = '';
-
- static override get styles() {
- return [
- grFormStyles,
- css`
- :host {
- display: block;
- margin-bottom: var(--spacing-xxl);
- }
- `,
- ];
- }
-
- override render() {
- const anchor = this.anchor ?? '';
- return html`<h2 id=${anchor} class="heading-2">${this.title}</h2>`;
- }
-}
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts
deleted file mode 100644
index 6c83bea..0000000
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {pageNavStyles} from '../../../styles/gr-page-nav-styles';
-import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, html} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-settings-menu-item': GrSettingsMenuItem;
- }
-}
-
-@customElement('gr-settings-menu-item')
-export class GrSettingsMenuItem extends LitElement {
- @property({type: String})
- href?: string;
-
- @property({type: String})
- override title = '';
-
- static override get styles() {
- return [sharedStyles, pageNavStyles];
- }
-
- override render() {
- const href = this.href ?? '';
- return html` <div class="navStyles">
- <li><a href=${href}>${this.title}</a></li>
- </div>`;
- }
-}
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 e1ce5cc..7626e14 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
@@ -38,7 +38,7 @@
import {GrSshEditor} from '../gr-ssh-editor/gr-ssh-editor';
import {GrGpgEditor} from '../gr-gpg-editor/gr-gpg-editor';
import {GrEmailEditor} from '../gr-email-editor/gr-email-editor';
-import {fireAlert, fireTitleChange} from '../../../utils/event-util';
+import {fire, fireAlert, fireTitleChange} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
import {LitElement, css, html} from 'lit';
@@ -190,7 +190,7 @@
const message = await this.restApiService.confirmEmail(this.emailToken);
if (message) fireAlert(this, message);
this.getViewModel().clearToken();
- await this.emailEditor.loadData();
+ await this.getUserModel().loadEmails(true);
}
override connectedCallback() {
@@ -230,8 +230,6 @@
})
);
- promises.push(this.emailEditor.loadData());
-
this._testOnly_loadingPromise = Promise.all(promises).then(() => {
this.loading = false;
@@ -252,6 +250,7 @@
css`
:host {
color: var(--primary-text-color);
+ overflow: auto;
}
h2 {
font-family: var(--header-font-family);
@@ -340,6 +339,9 @@
@unsaved-changes-changed=${(e: ValueChangedEvent<boolean>) => {
this.accountInfoChanged = e.detail.value;
}}
+ @account-detail-update=${() => {
+ fire(this, 'account-detail-update', {});
+ }}
></gr-account-info>
<gr-button
@click=${() => {
@@ -469,8 +471,8 @@
}}
></gr-email-editor>
<gr-button
- @click=${() => {
- this.emailEditor.save();
+ @click=${async () => {
+ await this.emailEditor.save();
}}
?disabled=${!this.emailsChanged}
>Save changes</gr-button
@@ -603,7 +605,7 @@
};
reloadAccountDetail() {
- Promise.all([this.accountInfo.loadData(), this.emailEditor.loadData()]);
+ Promise.all([this.accountInfo.loadData()]);
}
// private but used in test
@@ -641,7 +643,7 @@
if (!this.isNewEmailValid(this.newEmail)) return;
this.addingEmail = true;
- this.restApiService.addAccountEmail(this.newEmail).then(response => {
+ this.restApiService.addAccountEmail(this.newEmail).then(async response => {
this.addingEmail = false;
// If it was unsuccessful.
@@ -651,6 +653,8 @@
this.lastSentVerificationEmail = this.newEmail;
this.newEmail = '';
+
+ await this.getUserModel().loadEmails(true);
});
}
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 b6690b6..74bcca9 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
@@ -331,12 +331,6 @@
assert.isNotOk(element.lastSentVerificationEmail);
});
- test('emails are loaded without emailToken', () => {
- const emailEditorLoadDataStub = sinon.stub(element.emailEditor, 'loadData');
- element.firstUpdated();
- assert.isTrue(emailEditorLoadDataStub.calledOnce);
- });
-
test('handleSaveChangeTable', () => {
let newColumns = ['Owner', 'Project', 'Branch'];
element.localChangeTableColumns = newColumns.slice(0);
@@ -387,10 +381,8 @@
value: string | PromiseLike<string | null> | null
) => void;
let confirmEmailStub: sinon.SinonStub;
- let emailEditorLoadDataStub: sinon.SinonStub;
setup(() => {
- emailEditorLoadDataStub = sinon.stub(element.emailEditor, 'loadData');
confirmEmailStub = stubRestApi('confirmEmail').returns(
new Promise(resolve => {
resolveConfirm = resolve;
@@ -406,16 +398,6 @@
assert.isTrue(confirmEmailStub.calledWith('foo'));
});
- test('emails are not loaded initially', () => {
- assert.isFalse(emailEditorLoadDataStub.called);
- });
-
- test('user emails are loaded after email confirmed', async () => {
- resolveConfirm('bar');
- await element._testOnly_loadingPromise;
- assert.isTrue(emailEditorLoadDataStub.calledOnce);
- });
-
test('show-alert is fired when email is confirmed', async () => {
const dispatchEventSpy = sinon.spy(element, 'dispatchEvent');
resolveConfirm('bar');
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 cc2d6bd..6f7e151 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -689,6 +689,10 @@
/* Making up for the 2px reduced height above. */
top: 1px;
}
+ gr-suggestion-diff-preview,
+ gr-fix-suggestions {
+ margin-top: var(--spacing-s);
+ }
`,
];
}
diff --git a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
index 63c3832..195eeb6 100644
--- a/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
+++ b/polygerrit-ui/app/elements/shared/gr-fix-suggestions/gr-fix-suggestions.ts
@@ -87,6 +87,9 @@
static override get styles() {
return [
css`
+ :host {
+ display: block;
+ }
.header {
background-color: var(--background-color-primary);
border: 1px solid var(--border-color);
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
index c533a5e..e1d4f89 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview.ts
@@ -152,9 +152,16 @@
static override get styles() {
return [
css`
+ :host {
+ display: block;
+ }
.buttons {
text-align: right;
}
+ .diff-container {
+ border: 1px solid var(--border-color);
+ border-top: none;
+ }
code {
max-width: var(--gr-formatted-text-prose-max-width, none);
background-color: var(--background-color-secondary);
@@ -221,14 +228,16 @@
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>`;
+ return html`<div class="diff-container">
+ <gr-diff
+ .prefs=${this.overridePartialDiffPrefs()}
+ .path=${this.preview.filepath}
+ .diff=${diff}
+ .layers=${this.layers}
+ .renderPrefs=${this.renderPrefs}
+ .viewMode=${DiffViewMode.UNIFIED}
+ ></gr-diff>
+ </div>`;
}
private async fetchFixPreview() {
diff --git a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
index 86be868..2630aad 100644
--- a/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-suggestion-diff-preview/gr-suggestion-diff-preview_test.ts
@@ -103,10 +103,12 @@
assert.shadowDom.equal(
element,
/* HTML */ `
- <gr-diff
- class="disable-context-control-buttons hide-line-length-indicator"
- >
- </gr-diff>
+ <div class="diff-container">
+ <gr-diff
+ class="disable-context-control-buttons hide-line-length-indicator"
+ >
+ </gr-diff>
+ </div>
`,
{ignoreAttributes: ['style']}
);
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
index 5074290..9337951 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
@@ -23,6 +23,7 @@
['application/typescript', 'typescript'],
['application/xml', 'xml'],
['application/xquery', 'xquery'],
+ ['application/x-epp', 'epp'],
['application/x-erb', 'erb'],
['text/css', 'css'],
['text/html', 'html'],
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index ed492c1..497962f 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -432,7 +432,7 @@
threads.filter(t => !isNewThread(t) && isDraftThread(t))
);
- public readonly threadsWithSuggestions$ = select(
+ public readonly threadsWithUnappliedSuggestions$ = select(
combineLatest([this.threads$, this.changeModel.latestPatchNum$]),
([threads, latestPs]) =>
threads.filter(
diff --git a/polygerrit-ui/app/models/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
index cd6a66a..4973307 100644
--- a/polygerrit-ui/app/models/user/user-model.ts
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {from, of, Observable} from 'rxjs';
-import {filter, switchMap} from 'rxjs/operators';
+import {filter, switchMap, tap} from 'rxjs/operators';
import {
DiffPreferencesInfo as DiffPreferencesInfoAPI,
DiffViewMode,
@@ -13,6 +13,7 @@
AccountCapabilityInfo,
AccountDetailInfo,
EditPreferencesInfo,
+ EmailInfo,
PreferencesInfo,
TopMenuItemInfo,
} from '../../types/common';
@@ -48,6 +49,7 @@
* `account` is known, then use `accountLoaded` below.
*/
account?: AccountDetailInfo;
+ emails?: EmailInfo[];
/**
* Starts as `false` and switches to `true` after the first `getAccount` call.
* A common use case for this is to wait with loading or doing something until
@@ -82,6 +84,15 @@
userState => userState.account
);
+ readonly emails$: Observable<EmailInfo[] | undefined> = select(
+ this.state$,
+ userState => userState.emails
+ ).pipe(
+ tap(emails => {
+ if (emails === undefined) this.loadEmails();
+ })
+ );
+
/**
* Only emits once we have tried to actually load the account. Note that
* this does not initially emit a value.
@@ -148,12 +159,8 @@
super({
accountLoaded: false,
});
+ this.loadAccount();
this.subscriptions = [
- from(this.restApiService.getAccount()).subscribe(
- (account?: AccountDetailInfo) => {
- this.setAccount(account);
- }
- ),
this.loadedAccount$
.pipe(
switchMap(account => {
@@ -261,4 +268,22 @@
setAccount(account?: AccountDetailInfo) {
this.updateState({account, accountLoaded: true});
}
+
+ private setAccountEmails(emails?: EmailInfo[]) {
+ this.updateState({emails});
+ }
+
+ loadAccount(noCache?: boolean) {
+ if (noCache) this.restApiService.invalidateAccountsDetailCache();
+ return this.restApiService.getAccount().then(account => {
+ this.setAccount(account);
+ });
+ }
+
+ loadEmails(noCache?: boolean) {
+ if (noCache) this.restApiService.invalidateAccountsEmailCache();
+ return this.restApiService.getAccountEmails().then(emails => {
+ this.setAccountEmails(emails);
+ });
+ }
}
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index 9fa2e89..21fab53 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -454,6 +454,14 @@
},
},
{
+ name: 'highlightjs-epp',
+ license: {
+ name: 'highlightjs-epp',
+ type: LicenseTypes.Bsd3,
+ packageLicenseFile: 'LICENSE',
+ },
+ },
+ {
name: 'highlightjs-structured-text',
license: {
name: 'highlightjs-structured-text',
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 73816d2..dea0e8b 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -35,8 +35,9 @@
"@webcomponents/shadycss": "^1.11.2",
"@webcomponents/webcomponentsjs": "^1.3.3",
"highlight.js": "^11.9.0",
- "highlightjs-closure-templates": "https://github.com/highlightjs/highlightjs-closure-templates",
- "highlightjs-structured-text": "https://github.com/highlightjs/highlightjs-structured-text",
+ "highlightjs-closure-templates": "https://github.com/highlightjs/highlightjs-closure-templates#02fb0646e0499084f96a99b8c6f4a0d7bd1d33ba",
+ "highlightjs-epp": "https://github.com/highlightjs/highlightjs-epp#9f9e1a92f37c217c68899c7d3bdccb4d134681b9",
+ "highlightjs-structured-text": "https://github.com/highlightjs/highlightjs-structured-text#e68dd7aa829529fb6c40d6287585f43273605a9e",
"immer": "^9.0.21",
"lit": "^3.1.2",
"polymer-bridges": "file:../../polymer-bridges",
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 0be3ab5..7a12e7a 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
@@ -1565,6 +1565,10 @@
this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/self/detail');
}
+ invalidateAccountsEmailCache() {
+ this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/self/emails');
+ }
+
getGroups(filter: string, groupsPerPage: number, offset?: number) {
const url = this._getGroupsUrl(filter, groupsPerPage, offset);
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 947952c..814e97a 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
@@ -542,6 +542,7 @@
invalidateReposCache(): void;
invalidateAccountsCache(): void;
invalidateAccountsDetailCache(): void;
+ invalidateAccountsEmailCache(): void;
removeFromAttentionSet(
changeNum: NumericChangeId,
user: AccountId,
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 77f2498..570b50a 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -442,6 +442,7 @@
invalidateGroupsCache(): void {},
invalidateReposCache(): void {},
invalidateAccountsDetailCache(): void {},
+ invalidateAccountsEmailCache(): void {},
probePath(): Promise<boolean> {
return Promise.resolve(true);
},
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index b2f722f..a8f10e2 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -610,18 +610,24 @@
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
-"highlight.js@^11.5.0 || ^10.4.1", highlight.js@^11.9.0:
+highlight.js@^11.9.0, "highlight.js@^11.9.0 || ^10.4.1":
version "11.9.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
-"highlightjs-closure-templates@https://github.com/highlightjs/highlightjs-closure-templates":
+"highlightjs-closure-templates@https://github.com/highlightjs/highlightjs-closure-templates#02fb0646e0499084f96a99b8c6f4a0d7bd1d33ba":
version "0.0.1"
- resolved "https://github.com/highlightjs/highlightjs-closure-templates#7922b1e68def8b10199e186bb679600de3ebb711"
+ resolved "https://github.com/highlightjs/highlightjs-closure-templates#02fb0646e0499084f96a99b8c6f4a0d7bd1d33ba"
dependencies:
- highlight.js "^11.5.0 || ^10.4.1"
+ highlight.js "^11.9.0 || ^10.4.1"
-"highlightjs-structured-text@https://github.com/highlightjs/highlightjs-structured-text":
+"highlightjs-epp@https://github.com/highlightjs/highlightjs-epp#9f9e1a92f37c217c68899c7d3bdccb4d134681b9":
+ version "0.0.1"
+ resolved "https://github.com/highlightjs/highlightjs-epp#9f9e1a92f37c217c68899c7d3bdccb4d134681b9"
+ dependencies:
+ highlight.js "^11.9.0"
+
+"highlightjs-structured-text@https://github.com/highlightjs/highlightjs-structured-text#e68dd7aa829529fb6c40d6287585f43273605a9e":
version "1.4.9"
resolved "https://github.com/highlightjs/highlightjs-structured-text#e68dd7aa829529fb6c40d6287585f43273605a9e"
dependencies:
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index 642ef474..432d088 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -63,6 +63,7 @@
el = text/x-common-lisp
elm = text/x-elm
ejs = application/x-ejs
+epp = application/x-epp
erb = application/x-erb
erl = text/x-erlang
es6 = text/jsx