Merge "Fix exception for Revert Submission"
diff --git a/Jenkinsfile b/Jenkinsfile
index 0193df3..f21c7897 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -185,9 +185,17 @@
def collectBuilds() {
def builds = [:]
- builds["Gerrit-codestyle"] = prepareBuildsForMode("Gerrit-codestyle")
- Builds.modes.each {
- builds["Gerrit-verification(${it})"] = prepareBuildsForMode("Gerrit-verifier-bazel", it)
+ if (env.GERRIT_CHANGE_NUMBER == "") {
+ builds["java8"] = { -> build "Gerrit-bazel-${env.BRANCH_NAME}" }
+
+ if (env.BRANCH_NAME == "master") {
+ builds["java11"] = { -> build "Gerrit-bazel-java11-${env.BRANCH_NAME}" }
+ }
+ } else {
+ builds["Gerrit-codestyle"] = prepareBuildsForMode("Gerrit-codestyle")
+ Builds.modes.each {
+ builds["Gerrit-verification(${it})"] = prepareBuildsForMode("Gerrit-verifier-bazel", it)
+ }
}
return builds
}
@@ -274,44 +282,48 @@
node ('master') {
- stage('Preparing'){
- gerritReview labels: ['Verified': 0, 'Code-Style': 0]
+ if (env.GERRIT_CHANGE_NUMBER != "") {
+ stage('Preparing'){
+ gerritReview labels: ['Verified': 0, 'Code-Style': 0]
- getChangeMetaData()
- collectBuildModes()
+ getChangeMetaData()
+ collectBuildModes()
+ }
}
parallel(collectBuilds())
- stage('Retry Flaky Builds'){
- def flakyBuildsModes = findFlakyBuilds()
- if (flakyBuildsModes.size() > 0){
- parallel flakyBuildsModes.collectEntries {
- ["Gerrit-verification(${it})" :
- prepareBuildsForMode("Gerrit-verifier-bazel", it, 3)]
+ if (env.GERRIT_CHANGE_NUMBER != "") {
+ stage('Retry Flaky Builds'){
+ def flakyBuildsModes = findFlakyBuilds()
+ if (flakyBuildsModes.size() > 0){
+ parallel flakyBuildsModes.collectEntries {
+ ["Gerrit-verification(${it})" :
+ prepareBuildsForMode("Gerrit-verifier-bazel", it, 3)]
+ }
}
}
- }
- stage('Report to Gerrit'){
- resCodeStyle = getLabelValue(1, Builds.codeStyle.result)
- gerritReview(
- labels: ['Code-Style': resCodeStyle],
- message: createCodeStyleMsgBody(Builds.codeStyle, resCodeStyle))
- postCheck(new GerritCheck("codestyle", Change.number, Change.sha1, Builds.codeStyle))
+ stage('Report to Gerrit'){
+ resCodeStyle = getLabelValue(1, Builds.codeStyle.result)
+ gerritReview(
+ labels: ['Code-Style': resCodeStyle],
+ message: createCodeStyleMsgBody(Builds.codeStyle, resCodeStyle))
+ postCheck(new GerritCheck("codestyle", Change.number, Change.sha1, Builds.codeStyle))
- def verificationResults = Builds.verification.collect { k, v -> v }
- def resVerify = verificationResults.inject(1) {
- acc, build -> getLabelValue(acc, build.result)
+ def verificationResults = Builds.verification.collect { k, v -> v }
+ def resVerify = verificationResults.inject(1) {
+ acc, build -> getLabelValue(acc, build.result)
+ }
+ gerritReview(
+ labels: ['Verified': resVerify],
+ message: createVerifyMsgBody(Builds.verification))
+
+ Builds.verification.each { type, build -> postCheck(
+ new GerritCheck(type, Change.number, Change.sha1, build)
+ )}
+
+ setResult(resVerify, resCodeStyle)
}
- gerritReview(
- labels: ['Verified': resVerify],
- message: createVerifyMsgBody(Builds.verification))
-
- Builds.verification.each { type, build -> postCheck(
- new GerritCheck(type, Change.number, Change.sha1, build)
- )}
-
- setResult(resVerify, resCodeStyle)
}
}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
index a378fa4..aa1b921 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
@@ -19,7 +19,7 @@
import com.google.gerrit.extensions.registration.PluginName;
import com.google.gerrit.httpd.restapi.RestApiServlet.ViewData;
import com.google.gerrit.metrics.Counter1;
-import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.Field;
@@ -37,7 +37,7 @@
};
final Counter1<String> count;
- final Counter2<String, Integer> errorCount;
+ final Counter3<String, Integer, String> errorCount;
final Timer1<String> serverLatency;
final Histogram1<String> responseBytes;
@@ -60,6 +60,9 @@
viewField,
Field.ofInteger("error_code", Metadata.Builder::httpStatus)
.description("HTTP status code")
+ .build(),
+ Field.ofString("cause", Metadata.Builder::cause)
+ .description("The cause of the error.")
.build());
serverLatency =
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 1514df8..0535397 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.flogger.LazyArgs.lazy;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
@@ -103,6 +104,7 @@
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.OptionUtil;
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
@@ -173,6 +175,7 @@
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.GZIPOutputStream;
@@ -245,6 +248,7 @@
final DynamicSet<PerformanceLogger> performanceLoggers;
final ChangeFinder changeFinder;
final RetryHelper retryHelper;
+ final PluginSetContext<ExceptionHook> exceptionHooks;
@Inject
Globals(
@@ -259,7 +263,8 @@
@GerritServerConfig Config config,
DynamicSet<PerformanceLogger> performanceLoggers,
ChangeFinder changeFinder,
- RetryHelper retryHelper) {
+ RetryHelper retryHelper,
+ PluginSetContext<ExceptionHook> exceptionHooks) {
this.currentUser = currentUser;
this.webSession = webSession;
this.paramParser = paramParser;
@@ -272,6 +277,7 @@
this.performanceLoggers = performanceLoggers;
this.changeFinder = changeFinder;
this.retryHelper = retryHelper;
+ this.exceptionHooks = exceptionHooks;
allowOrigin = makeAllowOrigin(config);
}
@@ -286,7 +292,6 @@
private final Globals globals;
private final Provider<RestCollection<RestResource, RestResource>> members;
- private Optional<String> traceId = Optional.empty();
public RestApiServlet(
Globals globals, RestCollection<? extends RestResource, ? extends RestResource> members) {
@@ -312,6 +317,7 @@
res.setHeader("X-Content-Type-Options", "nosniff");
int status = SC_OK;
long responseBytes = -1;
+ Optional<Exception> cause = Optional.empty();
Response<?> response = null;
QueryParams qp = null;
Object inputRequestBody = null;
@@ -593,22 +599,28 @@
}
}
} catch (MalformedJsonException | JsonParseException e) {
+ cause = Optional.of(e);
responseBytes =
replyError(
req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
} catch (BadRequestException e) {
+ cause = Optional.of(e);
responseBytes =
replyError(
req, res, status = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
} catch (AuthException e) {
+ cause = Optional.of(e);
responseBytes =
replyError(req, res, status = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e);
} catch (AmbiguousViewException e) {
+ cause = Optional.of(e);
responseBytes = replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
} catch (ResourceNotFoundException e) {
+ cause = Optional.of(e);
responseBytes =
replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
} catch (MethodNotAllowedException e) {
+ cause = Optional.of(e);
responseBytes =
replyError(
req,
@@ -618,9 +630,11 @@
e.caching(),
e);
} catch (ResourceConflictException e) {
+ cause = Optional.of(e);
responseBytes =
replyError(req, res, status = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
} catch (PreconditionFailedException e) {
+ cause = Optional.of(e);
responseBytes =
replyError(
req,
@@ -630,6 +644,7 @@
e.caching(),
e);
} catch (UnprocessableEntityException e) {
+ cause = Optional.of(e);
responseBytes =
replyError(
req,
@@ -639,19 +654,22 @@
e.caching(),
e);
} catch (NotImplementedException e) {
+ cause = Optional.of(e);
logger.atSevere().withCause(e).log("Error in %s %s", req.getMethod(), uriForLogging(req));
responseBytes =
replyError(req, res, status = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
} catch (UpdateException e) {
+ cause = Optional.of(e);
Throwable t = e.getCause();
if (t instanceof LockFailureException) {
logger.atSevere().withCause(t).log("Error in %s %s", req.getMethod(), uriForLogging(req));
responseBytes = replyError(req, res, status = SC_SERVICE_UNAVAILABLE, "Lock failure", e);
} else {
status = SC_INTERNAL_SERVER_ERROR;
- responseBytes = handleException(e, req, res);
+ responseBytes = handleException(traceContext, e, req, res);
}
} catch (QuotaException e) {
+ cause = Optional.of(e);
responseBytes =
replyError(
req,
@@ -661,13 +679,15 @@
e.caching(),
e);
} catch (Exception e) {
+ cause = Optional.of(e);
status = SC_INTERNAL_SERVER_ERROR;
- responseBytes = handleException(e, req, res);
+ responseBytes = handleException(traceContext, e, req, res);
} finally {
String metric = getViewName(viewData);
+ String formattedCause = cause.map(globals.retryHelper::formatCause).orElse("_none");
globals.metrics.count.increment(metric);
if (status >= SC_BAD_REQUEST) {
- globals.metrics.errorCount.increment(metric, status);
+ globals.metrics.errorCount.increment(metric, status, formattedCause);
}
if (responseBytes != -1) {
globals.metrics.responseBytes.record(metric, responseBytes);
@@ -794,6 +814,7 @@
ActionType actionType,
Action<T> action)
throws Exception {
+ AtomicReference<Optional<String>> traceId = new AtomicReference<>(Optional.empty());
RetryHelper.Options.Builder retryOptionsBuilder = RetryHelper.options().caller(caller);
if (!traceContext.isTracing()) {
// enable automatic retry with tracing in case of non-recoverable failure
@@ -802,7 +823,7 @@
.retryWithTrace(t -> !(t instanceof RestApiException))
.onAutoTrace(
autoTraceId -> {
- traceId = Optional.of(autoTraceId);
+ traceId.set(Optional.of(autoTraceId));
// Include details of the request into the trace.
traceRequestData(req);
@@ -818,7 +839,9 @@
// If auto-tracing got triggered due to a non-recoverable failure, also trace the rest of
// this request. This means logging is forced for all further log statements and the logs are
// associated with the same trace ID.
- traceId.ifPresent(tid -> traceContext.addTag(RequestId.Type.TRACE_ID, tid).forceLogging());
+ traceId
+ .get()
+ .ifPresent(tid -> traceContext.addTag(RequestId.Type.TRACE_ID, tid).forceLogging());
}
}
@@ -1640,13 +1663,25 @@
}
}
- private long handleException(Throwable err, HttpServletRequest req, HttpServletResponse res)
+ private long handleException(
+ TraceContext traceContext, Throwable err, HttpServletRequest req, HttpServletResponse res)
throws IOException {
logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uriForLogging(req));
if (!res.isCommitted()) {
res.reset();
- traceId.ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
- return replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err);
+ traceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+ StringBuilder msg = new StringBuilder("Internal server error");
+ ImmutableList<String> userMessages =
+ globals.exceptionHooks.stream()
+ .map(h -> h.getUserMessage(err))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(toImmutableList());
+ if (!userMessages.isEmpty()) {
+ msg.append("\n");
+ userMessages.forEach(m -> msg.append("\n* ").append(m));
+ }
+ return replyError(req, res, SC_INTERNAL_SERVER_ERROR, msg.toString(), err);
}
return 0;
}
diff --git a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
index 996257c..5e7919f 100644
--- a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
+++ b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
@@ -115,11 +115,12 @@
.collect(toList()));
config.replace(createGroupAccessSection);
} else {
- Permission createGroupPermission = new Permission(Permission.CREATE);
- createGroupAccessSection.addPermission(createGroupPermission);
- createGroupsGlobal.forEach(createGroupPermission::add);
// The create permission is managed by Gerrit at this point only so there is no concern of
// overwriting user-defined permissions here.
+ Permission createGroupPermission = new Permission(Permission.CREATE);
+ createGroupAccessSection.remove(createGroupPermission);
+ createGroupAccessSection.addPermission(createGroupPermission);
+ createGroupsGlobal.forEach(createGroupPermission::add);
config.replace(createGroupAccessSection);
}
diff --git a/java/com/google/gerrit/server/ExceptionHook.java b/java/com/google/gerrit/server/ExceptionHook.java
index 6f05814..db44b4b 100644
--- a/java/com/google/gerrit/server/ExceptionHook.java
+++ b/java/com/google/gerrit/server/ExceptionHook.java
@@ -53,4 +53,15 @@
default Optional<String> formatCause(Throwable throwable) {
return Optional.empty();
}
+
+ /**
+ * Returns an error message that should be returned to the user.
+ *
+ * @param throwable throwable that was thrown while executing an operation
+ * @return error message that should be returned to the user, {@link Optional#empty()} if no
+ * message should be returned to the user
+ */
+ default Optional<String> getUserMessage(Throwable throwable) {
+ return Optional.empty();
+ }
}
diff --git a/java/com/google/gerrit/server/ExceptionHookImpl.java b/java/com/google/gerrit/server/ExceptionHookImpl.java
index b5edb24..9613b29 100644
--- a/java/com/google/gerrit/server/ExceptionHookImpl.java
+++ b/java/com/google/gerrit/server/ExceptionHookImpl.java
@@ -14,9 +14,9 @@
package com.google.gerrit.server;
-import com.google.gerrit.exceptions.StorageException;
+import com.google.common.base.Predicate;
+import com.google.common.base.Throwables;
import com.google.gerrit.git.LockFailureException;
-import com.google.gerrit.server.update.UpdateException;
import java.util.Optional;
import org.eclipse.jgit.lib.RefUpdate;
@@ -25,6 +25,11 @@
* a retry of the failed operation.
*/
public class ExceptionHookImpl implements ExceptionHook {
+ private static final String LOCK_FAILURE_USER_MESSAGE =
+ "Updating a ref failed with LOCK_FAILURE.\n"
+ + "This may be a temporary issue due to concurrent updates.\n"
+ + "Please retry later.";
+
@Override
public boolean shouldRetry(Throwable throwable) {
return isLockFailure(throwable);
@@ -38,10 +43,26 @@
return Optional.empty();
}
- private static boolean isLockFailure(Throwable throwable) {
- if (throwable instanceof UpdateException || throwable instanceof StorageException) {
- throwable = throwable.getCause();
+ @Override
+ public Optional<String> getUserMessage(Throwable throwable) {
+ if (isLockFailure(throwable)) {
+ return Optional.of(LOCK_FAILURE_USER_MESSAGE);
}
- return throwable instanceof LockFailureException;
+ return Optional.empty();
+ }
+
+ private static boolean isLockFailure(Throwable throwable) {
+ return isMatching(throwable, t -> t instanceof LockFailureException);
+ }
+
+ /**
+ * Check whether the given exception or any of its causes matches the given predicate.
+ *
+ * @param throwable Exception that should be tested
+ * @param predicate predicate to check if a throwable matches
+ * @return {@code true} if the given exception or any of its causes matches the given predicate
+ */
+ private static boolean isMatching(Throwable throwable, Predicate<Throwable> predicate) {
+ return Throwables.getCausalChain(throwable).stream().anyMatch(predicate);
}
}
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index c8b5e3f..9aebebf 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -52,6 +52,7 @@
MergedByPushOp create(
RequestScopePropagator requestScopePropagator,
PatchSet.Id psId,
+ @Assisted RequestId submissionId,
@Assisted("refName") String refName,
@Assisted("mergeResultRevId") String mergeResultRevId);
}
@@ -65,6 +66,7 @@
private final ChangeMerged changeMerged;
private final PatchSet.Id psId;
+ private final RequestId submissionId;
private final String refName;
private final String mergeResultRevId;
@@ -84,6 +86,7 @@
ChangeMerged changeMerged,
@Assisted RequestScopePropagator requestScopePropagator,
@Assisted PatchSet.Id psId,
+ @Assisted RequestId submissionId,
@Assisted("refName") String refName,
@Assisted("mergeResultRevId") String mergeResultRevId) {
this.patchSetInfoFactory = patchSetInfoFactory;
@@ -93,6 +96,7 @@
this.sendEmailExecutor = sendEmailExecutor;
this.changeMerged = changeMerged;
this.requestScopePropagator = requestScopePropagator;
+ this.submissionId = submissionId;
this.psId = psId;
this.refName = refName;
this.mergeResultRevId = mergeResultRevId;
@@ -133,7 +137,6 @@
}
change.setCurrentPatchSet(info);
change.setStatus(Change.Status.MERGED);
- RequestId submissionId = new RequestId(change.getId().toString());
change.setSubmissionId(submissionId.toStringForStorage());
// we cannot reconstruct the submit records for when this change was
// submitted, this is why we must fix the status and other details.
diff --git a/java/com/google/gerrit/server/git/TracingHook.java b/java/com/google/gerrit/server/git/TracingHook.java
index 63d8bc6..56eded0 100644
--- a/java/com/google/gerrit/server/git/TracingHook.java
+++ b/java/com/google/gerrit/server/git/TracingHook.java
@@ -14,8 +14,6 @@
package com.google.gerrit.server.git;
-import static com.google.common.base.Preconditions.checkState;
-
import com.google.gerrit.server.logging.TraceContext;
import java.util.List;
import java.util.Optional;
@@ -59,7 +57,10 @@
* @param serverOptionList list of provided server options
*/
private void maybeStartTrace(List<String> serverOptionList) {
- checkState(traceContext == null, "Trace was already started.");
+ if (traceContext != null) {
+ // Trace was already started
+ return;
+ }
Optional<String> traceOption = parseTraceOption(serverOptionList);
traceContext =
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 7dd21e1..cec9e4e 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -3281,6 +3281,7 @@
int existingPatchSets = 0;
int newPatchSets = 0;
+ RequestId submissionId = null;
COMMIT:
for (RevCommit c; (c = rw.next()) != null; ) {
rw.parseBody(c);
@@ -3289,13 +3290,20 @@
receivePackRefCache.tipsFromObjectId(c.copy(), RefNames.REFS_CHANGES)) {
PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
Optional<ChangeNotes> notes = getChangeNotes(psId.changeId());
+ if (submissionId == null) {
+ submissionId = new RequestId(psId.changeId().toString());
+ }
if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) {
existingPatchSets++;
bu.addOp(notes.get().getChangeId(), setPrivateOpFactory.create(false, null));
bu.addOp(
psId.changeId(),
mergedByPushOpFactory.create(
- requestScopePropagator, psId, refName, newTip.getId().getName()));
+ requestScopePropagator,
+ psId,
+ submissionId,
+ refName,
+ newTip.getId().getName()));
continue COMMIT;
}
}
@@ -3324,13 +3332,20 @@
logger.atFine().log("Not closing %s because validation failed", id);
continue;
}
+ if (submissionId == null) {
+ submissionId = new RequestId(id.toString());
+ }
req.addOps(bu, null);
bu.addOp(id, setPrivateOpFactory.create(false, null));
bu.addOp(
id,
mergedByPushOpFactory
.create(
- requestScopePropagator, req.psId, refName, newTip.getId().getName())
+ requestScopePropagator,
+ req.psId,
+ submissionId,
+ refName,
+ newTip.getId().getName())
.setPatchSetProvider(req.replaceOp::getPatchSet));
bu.addOp(id, new ChangeProgressOp(progress));
ids.add(id);
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index e95cf3b..6c0d5d3 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -61,6 +61,7 @@
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.git.MergedByPushOp;
import com.google.gerrit.server.git.receive.ReceiveCommits.MagicBranchInput;
+import com.google.gerrit.server.logging.RequestId;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -236,7 +237,11 @@
if (mergedInto != null) {
mergedByPushOp =
mergedByPushOpFactory.create(
- requestScopePropagator, patchSetId, mergedInto, mergeResultRevId);
+ requestScopePropagator,
+ patchSetId,
+ new RequestId(patchSetId.changeId().toString()),
+ mergedInto,
+ mergeResultRevId);
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 4d551a2..fa877af 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -923,9 +923,18 @@
lowerNames.put(lower, name);
List<LabelValue> values = new ArrayList<>();
+ Set<Short> allValues = new HashSet<>();
for (String value : rc.getStringList(LABEL, name, KEY_VALUE)) {
try {
- values.add(parseLabelValue(value));
+ LabelValue labelValue = parseLabelValue(value);
+ if (allValues.add(labelValue.getValue())) {
+ values.add(labelValue);
+ } else {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ String.format("Duplicate %s \"%s\" for label \"%s\"", KEY_VALUE, value, name)));
+ }
} catch (IllegalArgumentException notValue) {
error(
new ValidationError(
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
index 8040847..af65483 100644
--- a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toMap;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
@@ -76,7 +77,15 @@
// Map of all patch sets, keyed by commit SHA-1.
Map<ObjectId, PatchSetData> byId = collectById(in);
PatchSetData start = byId.get(startPs.commitId());
- checkArgument(start != null, "%s not found in %s", startPs, in);
+ requireNonNull(
+ start,
+ () ->
+ String.format(
+ "commit %s of patch set %s not found in %s",
+ startPs.commitId().name(),
+ startPs.id(),
+ byId.entrySet().stream()
+ .collect(toMap(e -> e.getKey().name(), e -> e.getValue().patchSet().id()))));
// Map of patch set -> immediate parent.
ListMultimap<PatchSetData, PatchSetData> parents =
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 9d2bd03..b44bb29 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -161,10 +161,21 @@
"Revert this change and all changes that have been submitted together with this change")
.setVisible(
and(
- change.isMerged() && change.getSubmissionId() != null && projectStatePermitsWrite,
+ change.isMerged()
+ && change.getSubmissionId() != null
+ && isChangePartOfSubmission(change.getSubmissionId())
+ && projectStatePermitsWrite,
permissionBackend
.user(rsrc.getUser())
.ref(change.getDest())
.testCond(CREATE_CHANGE)));
}
+
+ /**
+ * @param submissionId the submission id of the change.
+ * @return True if the submission has more than one change, false otherwise.
+ */
+ private Boolean isChangePartOfSubmission(String submissionId) {
+ return (queryProvider.get().setLimit(2).bySubmissionId(submissionId).size() > 1);
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
index a45c67f..1e288f4 100644
--- a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
+++ b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
@@ -24,10 +24,12 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.server.project.RefPattern;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
+import java.util.Set;
public class LabelDefinitionInputParser {
public static LabelFunction parseFunction(String functionString) throws BadRequestException {
@@ -39,6 +41,7 @@
public static List<LabelValue> parseValues(Map<String, String> values)
throws BadRequestException {
List<LabelValue> valueList = new ArrayList<>();
+ Set<Short> allValues = new HashSet<>();
for (Entry<String, String> e : values.entrySet()) {
short value;
try {
@@ -46,6 +49,9 @@
} catch (NumberFormatException ex) {
throw new BadRequestException("invalid value: " + e.getKey(), ex);
}
+ if (!allValues.add(value)) {
+ throw new BadRequestException("duplicate value: " + value);
+ }
String valueDescription = e.getValue().trim();
if (valueDescription.isEmpty()) {
throw new BadRequestException("description for value '" + e.getKey() + "' cannot be empty");
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index 6f8ef12..4f9a67c 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -34,8 +34,6 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.metrics.Counter1;
-import com.google.gerrit.metrics.Counter2;
import com.google.gerrit.metrics.Counter3;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Field;
@@ -128,8 +126,8 @@
@VisibleForTesting
@Singleton
public static class Metrics {
- final Counter2<ActionType, String> attemptCounts;
- final Counter1<ActionType> timeoutCount;
+ final Counter3<ActionType, String, String> attemptCounts;
+ final Counter3<ActionType, String, String> timeoutCount;
final Counter3<ActionType, String, String> autoRetryCount;
final Counter3<ActionType, String, String> failuresOnAutoRetryCount;
@@ -137,6 +135,19 @@
Metrics(MetricMaker metricMaker) {
Field<ActionType> actionTypeField =
Field.ofEnum(ActionType.class, "action_type", Metadata.Builder::actionType).build();
+ Field<String> operationNameField =
+ Field.ofString("operation_name", Metadata.Builder::operationName)
+ .description("The name of the operation that was retried.")
+ .build();
+ Field<String> lastAttemptCauseField =
+ Field.ofString("cause", Metadata.Builder::cause)
+ .description("The cause for the last attempt.")
+ .build();
+ Field<String> causeField =
+ Field.ofString("cause", Metadata.Builder::cause)
+ .description("The cause for the retry.")
+ .build();
+
attemptCounts =
metricMaker.newCounter(
"action/retry_attempt_count",
@@ -146,9 +157,8 @@
.setCumulative()
.setUnit("attempts"),
actionTypeField,
- Field.ofString("cause", Metadata.Builder::cause)
- .description("The cause for the last attempt.")
- .build());
+ operationNameField,
+ lastAttemptCauseField);
timeoutCount =
metricMaker.newCounter(
"action/retry_timeout_count",
@@ -156,7 +166,9 @@
"Number of action executions of RetryHelper that ultimately timed out")
.setCumulative()
.setUnit("timeouts"),
- actionTypeField);
+ actionTypeField,
+ operationNameField,
+ lastAttemptCauseField);
autoRetryCount =
metricMaker.newCounter(
"action/auto_retry_count",
@@ -164,12 +176,8 @@
.setCumulative()
.setUnit("retries"),
actionTypeField,
- Field.ofString("operation_name", Metadata.Builder::operationName)
- .description("The name of the operation that was retried.")
- .build(),
- Field.ofString("cause", Metadata.Builder::cause)
- .description("The cause for the retry.")
- .build());
+ operationNameField,
+ causeField);
failuresOnAutoRetryCount =
metricMaker.newCounter(
"action/failures_on_auto_retry_count",
@@ -177,12 +185,8 @@
.setCumulative()
.setUnit("failures"),
actionTypeField,
- Field.ofString("operation_name", Metadata.Builder::operationName)
- .description("The name of the operation that was retried.")
- .build(),
- Field.ofString("cause", Metadata.Builder::cause)
- .description("The cause for the retry.")
- .build());
+ operationNameField,
+ causeField);
}
}
@@ -355,19 +359,20 @@
return false;
});
retryerBuilder.withRetryListener(listener);
- return executeWithTimeoutCount(actionType, action, retryerBuilder.build());
+ return executeWithTimeoutCount(actionType, action, opts, retryerBuilder.build());
} finally {
if (listener.getAttemptCount() > 1) {
logger.atFine().log("%s was attempted %d times", actionType, listener.getAttemptCount());
metrics.attemptCounts.incrementBy(
actionType,
+ opts.caller().orElse("N/A"),
listener.getCause().map(this::formatCause).orElse("_unknown"),
listener.getAttemptCount() - 1);
}
}
}
- private String formatCause(Throwable t) {
+ public String formatCause(Throwable t) {
while ((t instanceof UpdateException
|| t instanceof StorageException
|| t instanceof ExecutionException)
@@ -396,18 +401,22 @@
*
* @param actionType the type of the action
* @param action the action which should be executed and retried on failure
+ * @param opts options for retrying the action on failure
* @param retryer the retryer
* @return the result of executing the action
* @throws Throwable any error or exception that made the action fail, callers are expected to
* catch and inspect this Throwable to decide carefully whether it should be re-thrown
*/
- private <T> T executeWithTimeoutCount(ActionType actionType, Action<T> action, Retryer<T> retryer)
- throws Throwable {
+ private <T> T executeWithTimeoutCount(
+ ActionType actionType, Action<T> action, Options opts, Retryer<T> retryer) throws Throwable {
try {
return retryer.call(action::call);
} catch (ExecutionException | RetryException e) {
if (e instanceof RetryException) {
- metrics.timeoutCount.increment(actionType);
+ metrics.timeoutCount.increment(
+ actionType,
+ opts.caller().orElse("N/A"),
+ e.getCause() != null ? formatCause(e.getCause()) : "_unknown");
}
if (e.getCause() != null) {
throw e.getCause();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 7ecb07e..477ec38 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3573,6 +3573,28 @@
}
@Test
+ public void checkSubmissionIdForAutoClosedChange() throws Exception {
+ PushOneCommit.Result first = createChange();
+ PushOneCommit.Result second = createChange();
+
+ PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
+
+ PushOneCommit.Result result = push.to("refs/heads/master");
+ result.assertOkStatus();
+
+ ChangeInfo firstChange = gApi.changes().id(first.getChangeId()).get();
+ assertThat(firstChange.status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(firstChange.submissionId).isNotNull();
+
+ ChangeInfo secondChange = gApi.changes().id(second.getChangeId()).get();
+ assertThat(secondChange.status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(secondChange.submissionId).isNotNull();
+
+ assertThat(secondChange.submissionId).isEqualTo(firstChange.submissionId);
+ assertThat(gApi.changes().id(second.getChangeId()).submittedTogether()).hasSize(2);
+ }
+
+ @Test
public void maxPermittedValueAllowed() throws Exception {
final int minPermittedValue = -2;
final int maxPermittedValue = +2;
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 78c8209..2801b36 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -69,11 +69,16 @@
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.index.IndexExecutor;
import com.google.gerrit.server.project.CommentLinkInfoImpl;
+import com.google.gerrit.server.project.ProjectConfig;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Module;
import java.util.HashMap;
import java.util.Map;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
@@ -851,6 +856,58 @@
assertCommentLinks(getConfig(project), expected);
}
+ @Test
+ public void cannotPushLabelDefinitionWithDuplicateValues() throws Exception {
+ Config cfg = new Config();
+ cfg.fromText(projectOperations.project(allProjects).getConfig().toText());
+ cfg.setStringList(
+ "label",
+ "Code-Review",
+ "value",
+ ImmutableList.of("+1 LGTM", "1 LGTM", "0 No Value", "-1 Looks Bad"));
+
+ TestRepository<InMemoryRepository> repo = cloneProject(allProjects);
+ GitUtil.fetch(repo, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG);
+ repo.reset(RefNames.REFS_CONFIG);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), repo, "Subject", "project.config", cfg.toText())
+ .to(RefNames.REFS_CONFIG);
+ r.assertErrorStatus("invalid project configuration");
+ r.assertMessage("project.config: duplicate value \"1 lgtm\" for label \"code-review\"");
+ }
+
+ @Test
+ public void getProjectThatHasLabelDefinitionWithDuplicateValues() throws Exception {
+ // Update the definition of the Code-Review label so that it has the value "+1 LGTM" twice.
+ // This update bypasses all validation checks so that the duplicate label value doesn't get
+ // rejected.
+ Config cfg = new Config();
+ cfg.fromText(projectOperations.project(allProjects).getConfig().toText());
+ cfg.setStringList(
+ "label",
+ "Code-Review",
+ "value",
+ ImmutableList.of("+1 LGTM", "1 LGTM", "0 No Value", "-1 Looks Bad"));
+
+ try (TestRepository<Repository> repo =
+ new TestRepository<>(repoManager.openRepository(allProjects))) {
+ repo.update(
+ RefNames.REFS_CONFIG,
+ repo.commit()
+ .message("Set label with duplicate value")
+ .parent(getHead(repo.getRepository(), RefNames.REFS_CONFIG))
+ .add(ProjectConfig.PROJECT_CONFIG, cfg.toText()));
+ }
+
+ // Verify that project info can be retrieved and that the label value "+1 LGTM" appears only
+ // once.
+ ProjectInfo projectInfo = gApi.projects().name(allProjects.get()).get();
+ assertThat(projectInfo.labels.keySet()).containsExactly("Code-Review");
+ assertThat(projectInfo.labels.get("Code-Review").values)
+ .containsExactly("+1", "LGTM", " 0", "No Value", "-1", "Looks Bad");
+ }
+
private CommentLinkInfo commentLinkInfo(String name, String match, String link) {
return new CommentLinkInfoImpl(name, match, link, null /*html*/, null /*enabled*/);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 8b51e7f..911a04d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -72,13 +72,28 @@
}
@Test
- public void changeActionOneMergedChangeHasReverts() throws Exception {
+ public void changeActionOneMergedChangeHasOnlyNormalRevert() throws Exception {
String changeId = createChangeWithTopic().getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
gApi.changes().id(changeId).current().submit();
Map<String, ActionInfo> actions = getChangeActions(changeId);
assertThat(actions).containsKey("revert");
- assertThat(actions).containsKey("revert_submission");
+ assertThat(actions).doesNotContainKey("revert_submission");
+ }
+
+ @Test
+ public void changeActionTwoMergedChangesHaveReverts() throws Exception {
+ String changeId1 = createChangeWithTopic().getChangeId();
+ String changeId2 = createChangeWithTopic().getChangeId();
+ gApi.changes().id(changeId1).current().review(ReviewInput.approve());
+ gApi.changes().id(changeId2).current().review(ReviewInput.approve());
+ gApi.changes().id(changeId2).current().submit();
+ Map<String, ActionInfo> actions1 = getChangeActions(changeId1);
+ assertThat(actions1).containsKey("revert");
+ assertThat(actions1).containsKey("revert_submission");
+ Map<String, ActionInfo> actions2 = getChangeActions(changeId2);
+ assertThat(actions2).containsKey("revert");
+ assertThat(actions2).containsKey("revert_submission");
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 91a10ca..3e9b1f6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -604,7 +604,7 @@
}
@Test
- public void syncCreateGroupPermission() throws Exception {
+ public void syncCreateGroupPermission_addAndRemoveCreateGroupCapability() throws Exception {
// Grant CREATE_GROUP to Registered Users
ProjectAccessInput accessInput = newProjectAccessInput();
AccessSectionInfo accessSection = newAccessSectionInfo();
@@ -642,6 +642,44 @@
}
@Test
+ public void syncCreateGroupPermission_addCreateGroupCapabilityToMultipleGroups()
+ throws Exception {
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+
+ // Grant CREATE_GROUP to Registered Users
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+ PermissionInfo createGroup = newPermissionInfo();
+ createGroup.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup);
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
+ gApi.projects().name(allProjects.get()).access(accessInput);
+
+ // Grant CREATE_GROUP to Administrators
+ accessInput = newProjectAccessInput();
+ accessSection = newAccessSectionInfo();
+ createGroup = newPermissionInfo();
+ createGroup.rules.put(adminGroupUuid().get(), pri);
+ accessSection.permissions.put(GlobalCapability.CREATE_GROUP, createGroup);
+ accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSection);
+ gApi.projects().name(allProjects.get()).access(accessInput);
+
+ // Assert that the permissions were synced from All-Projects (global) to All-Users (ref)
+ Map<String, AccessSectionInfo> local = gApi.projects().name("All-Users").access().local;
+ assertThat(local).isNotNull();
+ assertThat(local).containsKey(RefNames.REFS_GROUPS + "*");
+ Map<String, PermissionInfo> permissions = local.get(RefNames.REFS_GROUPS + "*").permissions;
+ assertThat(permissions).hasSize(2);
+ // READ is the default permission and should be preserved by the syncer
+ assertThat(permissions.keySet()).containsExactly(Permission.READ, Permission.CREATE);
+ Map<String, PermissionRuleInfo> rules = permissions.get(Permission.CREATE).rules;
+ assertThat(rules.keySet())
+ .containsExactly(SystemGroupBackend.REGISTERED_USERS.get(), adminGroupUuid().get());
+ assertThat(rules.get(SystemGroupBackend.REGISTERED_USERS.get())).isEqualTo(pri);
+ assertThat(rules.get(adminGroupUuid().get())).isEqualTo(pri);
+ }
+
+ @Test
public void addAccessSectionForInvalidRef() throws Exception {
ProjectAccessInput accessInput = newProjectAccessInput();
AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
index 28e8b14..57a1e56 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
@@ -169,6 +169,21 @@
}
@Test
+ public void cannotCreateLabelWithDuplicateValues() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ // Positive values can be specified as '<value>' or '+<value>'.
+ input.values =
+ ImmutableMap.of(
+ "+1", "Looks Good", "1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Foo").create(input));
+ assertThat(thrown).hasMessageThat().contains("duplicate value: 1");
+ }
+
+ @Test
public void cannotCreateLabelWithInvalidDefaultValue() throws Exception {
LabelDefinitionInput input = new LabelDefinitionInput();
input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
index 9cba930..97b795f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
@@ -323,6 +323,21 @@
}
@Test
+ public void cannotSetDuplicateValues() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ // Positive values can be specified as '<value>' or '+<value>'.
+ input.values =
+ ImmutableMap.of(
+ "+1", "Looks Good", "1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("duplicate value: 1");
+ }
+
+ @Test
public void updateDefaultValue() throws Exception {
LabelDefinitionInput input = new LabelDefinitionInput();
input.defaultValue = 1;
diff --git a/plugins/replication b/plugins/replication
index d7c09fb..4689b41 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit d7c09fbb4c18b1743d6060d361171c2a5237f22b
+Subproject commit 4689b419eab61ea204daf2dfca47296667ac317c
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index b7e615c..f1338fb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -92,7 +92,6 @@
a {
color: inherit;
cursor: pointer;
- display: inline-block;
text-decoration: none;
}
a:hover {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
index 0ec3d6a..2d66cfa 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
@@ -50,11 +50,7 @@
return url.pathname;
}
const base = Gerrit.BaseUrlBehavior.getBaseUrl();
- let pathname = url.pathname.replace(base, '');
- // Load from ASSETS_PATH
- if (window.ASSETS_PATH && url.href.includes(window.ASSETS_PATH)) {
- pathname = url.href.replace(window.ASSETS_PATH, '');
- }
+ const pathname = url.pathname.replace(base, '');
// Site theme is server from predefined path.
if (pathname === '/static/gerrit-theme.html') {
return 'gerrit-theme';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
index b43796f..128738d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
@@ -72,15 +72,6 @@
'gerrit-theme'
);
});
-
- test('with ASSETS_PATH', () => {
- window.ASSETS_PATH = 'http://cdn.com/2';
- assert.equal(
- getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`),
- 'a'
- );
- window.ASSETS_PATH = undefined;
- });
});
});
</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
index 96d8411..4be38b6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
@@ -113,7 +113,7 @@
this._pluginListLoaded = true;
plugins.forEach(path => {
- const url = this._urlFor(path, window.ASSETS_PATH);
+ const url = this._urlFor(path);
// Skip if preloaded, for bundling.
if (this.isPluginPreloaded(url)) return;
@@ -128,11 +128,11 @@
});
if (this._isPathEndsWith(url, '.html')) {
- this._importHtmlPlugin(path, opts && opts[path]);
+ this._importHtmlPlugin(url, opts && opts[path]);
} else if (this._isPathEndsWith(url, '.js')) {
- this._loadJsPlugin(path);
+ this._loadJsPlugin(url);
} else {
- this._failToLoad(`Unrecognized plugin path ${path}`, path);
+ this._failToLoad(`Unrecognized plugin url ${url}`, url);
}
});
@@ -181,15 +181,14 @@
return;
}
- const url = this._urlFor(src);
- const pluginObject = this.getPlugin(url);
+ const pluginObject = this.getPlugin(src);
let plugin = pluginObject && pluginObject.plugin;
if (!plugin) {
- plugin = new Plugin(url);
+ plugin = new Plugin(src);
}
try {
callback(plugin);
- this._pluginInstalled(url, plugin);
+ this._pluginInstalled(src, plugin);
} catch (e) {
this._failToLoad(`${e.name}: ${e.message}`, src);
}
@@ -314,79 +313,38 @@
}
_importHtmlPlugin(pluginUrl, opts = {}) {
- const urlWithAP = this._urlFor(pluginUrl, window.ASSETS_PATH);
- const urlWithoutAP = this._urlFor(pluginUrl);
- let onerror = null;
- if (urlWithAP !== urlWithoutAP) {
- onerror = () => this._loadHtmlPlugin(urlWithoutAP, opts.sync);
- }
- this._loadHtmlPlugin(urlWithAP, opts.sync, onerror);
- }
-
- _loadHtmlPlugin(url, sync, onerror) {
- if (!onerror) {
- onerror = () => {
- this._failToLoad(`${pluginUrl} import error`, pluginUrl);
- };
- }
-
+ // onload (second param) needs to be a function. When null or undefined
+ // were passed, plugins were not loaded correctly.
(Polymer.importHref || Polymer.Base.importHref)(
- url, () => {},
- onerror,
- !sync);
+ this._urlFor(pluginUrl), () => {},
+ () => this._failToLoad(`${pluginUrl} import error`, pluginUrl),
+ !opts.sync);
}
_loadJsPlugin(pluginUrl) {
- const urlWithAP = this._urlFor(pluginUrl, window.ASSETS_PATH);
- const urlWithoutAP = this._urlFor(pluginUrl);
- let onerror = null;
- if (urlWithAP !== urlWithoutAP) {
- onerror = () => this._createScriptTag(urlWithoutAP);
- }
-
- this._createScriptTag(urlWithAP, onerror);
+ this._createScriptTag(this._urlFor(pluginUrl));
}
- _createScriptTag(url, onerror) {
- if (!onerror) {
- onerror = () => this._failToLoad(`${url} load error`, url);
- }
-
+ _createScriptTag(url) {
const el = document.createElement('script');
el.defer = true;
el.setAttribute('src', url);
- el.onerror = onerror;
+ el.onerror = () => this._failToLoad(`${url} load error`, url);
return document.body.appendChild(el);
}
- _urlFor(pathOrUrl, assetsPath) {
+ _urlFor(pathOrUrl) {
if (!pathOrUrl) {
return pathOrUrl;
}
-
- // theme is per host, should always load from assetsPath
- const isThemeFile = pathOrUrl.endsWith('static/gerrit-theme.html');
- const shouldTryLoadFromAssetsPathFirst = !isThemeFile && assetsPath;
if (pathOrUrl.startsWith(PRELOADED_PROTOCOL) ||
pathOrUrl.startsWith('http')) {
// Plugins are loaded from another domain or preloaded.
- if (pathOrUrl.includes(location.host)
- && shouldTryLoadFromAssetsPathFirst) {
- // if is loading from host server, try replace with cdn when assetsPath provided
- return pathOrUrl
- .replace(location.origin, assetsPath);
- }
return pathOrUrl;
}
-
if (!pathOrUrl.startsWith('/')) {
pathOrUrl = '/' + pathOrUrl;
}
-
- if (shouldTryLoadFromAssetsPathFirst) {
- return assetsPath + pathOrUrl;
- }
-
return window.location.origin + getBaseUrl() + pathOrUrl;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
index 08e7e18..8c1ec96 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -325,11 +325,11 @@
let loadJsPluginStub;
setup(() => {
importHtmlPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
+ sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
importHtmlPluginStub(url);
});
loadJsPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
loadJsPluginStub(url);
});
});
@@ -346,8 +346,8 @@
assert.isTrue(failToLoadStub.calledOnce);
assert.isTrue(failToLoadStub.calledWithExactly(
- 'Unrecognized plugin path foo/bar',
- 'foo/bar'
+ `Unrecognized plugin url ${url}/foo/bar`,
+ `${url}/foo/bar`
));
});
@@ -407,72 +407,6 @@
});
});
- suite('With ASSETS_PATH', () => {
- let importHtmlPluginStub;
- let loadJsPluginStub;
- setup(() => {
- window.ASSETS_PATH = 'https://cdn.com';
- importHtmlPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => {
- importHtmlPluginStub(url);
- });
- loadJsPluginStub = sandbox.stub();
- sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => {
- loadJsPluginStub(url);
- });
- });
-
- teardown(() => {
- window.ASSETS_PATH = '';
- });
-
- test('Should try load plugins from assets path instead', () => {
- Gerrit._loadPlugins([
- 'foo/bar.js',
- 'foo/bar.html',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
- });
-
- test('Should honor original path if exists', () => {
- Gerrit._loadPlugins([
- 'http://e.com/foo/bar.html',
- 'http://e.com/foo/bar.js',
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`));
- });
-
- test('Should try replace current host with assetsPath', () => {
- const host = window.location.origin;
- Gerrit._loadPlugins([
- `${host}/foo/bar.html`,
- `${host}/foo/bar.js`,
- ]);
-
- assert.isTrue(importHtmlPluginStub.calledOnce);
- assert.isTrue(
- importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`)
- );
- assert.isTrue(loadJsPluginStub.calledOnce);
- assert.isTrue(
- loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`));
- });
- });
-
test('adds js plugins will call the body', () => {
Gerrit._loadPlugins([
'http://e.com/foo/bar.js',
@@ -555,10 +489,12 @@
test('installing preloaded plugin', () => {
let plugin;
+ window.ASSETS_PATH = 'http://blips.com/chitz';
Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
assert.strictEqual(plugin.getPluginName(), 'foo');
assert.strictEqual(plugin.url('/some/thing.html'),
- 'preloaded:foo/plugins/foo/some/thing.html');
+ 'http://blips.com/chitz/plugins/foo/some/thing.html');
+ delete window.ASSETS_PATH;
});
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 0aaeaa1..6c306d9 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -17,6 +17,8 @@
(function(window) {
'use strict';
+ const PRELOADED_PROTOCOL = 'preloaded:';
+
const PANEL_ENDPOINTS_MAPPING = {
CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK: 'change-view-integration',
CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK: 'change-metadata-item',
@@ -64,6 +66,13 @@
this._url = new URL(opt_url);
this._name = getPluginNameFromUrl(this._url);
+ if (this._url.protocol === PRELOADED_PROTOCOL) {
+ // Original plugin URL is used in plugin assets URLs calculation.
+ const assetsBaseUrl = window.ASSETS_PATH ||
+ (window.location.origin + Gerrit.BaseUrlBehavior.getBaseUrl());
+ this._url = new URL(assetsBaseUrl + '/plugins/' + this._name +
+ '/static/' + this._name + '.js');
+ }
}
Plugin._sharedAPIElement = document.createElement('gr-js-api-interface');