Merge "Check in reviewdb.proto instead of generating it"
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 82a9527..e84effd 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2670,6 +2670,94 @@
are met, but marked as `OK`. If the requirements were not displayed, reviewers
would need to use their precious time to manually check that they were met.
+[[quota-enforcer]]
+== Quota Enforcer
+
+Gerrit provides an extension point that allows a plugin to enforce quota.
+link:quota.html[This documentation page] has a list of all quota requests that
+Gerrit core issues. Plugins can choose to respond to all or just a subset of
+requests. Some implementations might want to keep track of user quota in buckets,
+others might just check against instance or project state to enforce limits on how
+many projects can be created or how large a repository can become.
+
+Checking against instance state can be racy for concurrent requests as the server does not
+refill tokens if the action fails in a later stage (e.g. database failure). If
+plugins want to guarantee an absolute maximum on a resource, they have to do their own
+book-keeping.
+
+[source, java]
+----
+import com.google.server.quota.QuotaEnforcer;
+
+class ProjectLimiter implements QuotaEnforcer {
+ private final long maxNumberOfProjects = 100;
+ @Override
+ QuotaResponse requestTokens(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+ if (!"/projects/create".equals(quotaGroup)) {
+ return QuotaResponse.noOp();
+ }
+ // No deduction because we always check against the instance state (racy but fine for
+ // this plugin)
+ if (currentNumberOfProjects() + numTokens > maxNumberOfProjects) {
+ return QuotaResponse.error("too many projects");
+ }
+ return QuotaResponse.ok();
+ }
+
+ @Override
+ QuotaResponse dryRun(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+ // Since we are not keeping any state in this enforcer, we can simply call requestTokens().
+ return requestTokens(quotaGroup, ctx, numTokens);
+ }
+
+ void refill(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+ // No-op
+ }
+}
+----
+
+[source, java]
+----
+import com.google.server.quota.QuotaEnforcer;
+
+class ApiQpsEnforcer implements QuotaEnforcer {
+ // AutoRefillingPerUserBuckets is a imaginary bucket implementation that could be based on
+ // a loading cache or a commonly used bucketing algorithm.
+ private final AutoRefillingPerUserBuckets<CurrentUser, Long> buckets;
+ @Override
+ QuotaResponse requestTokens(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+ if (!quotaGroup.startsWith("/restapi/")) {
+ return QuotaResponse.noOp();
+ }
+ boolean success = buckets.deduct(ctx.user(), numTokens);
+ if (!success) {
+ return QuotaResponse.error("user sent too many qps, please wait for 5 minutes");
+ }
+ return QuotaResponse.ok();
+ }
+
+ @Override
+ QuotaResponse dryRun(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+ if (!quotaGroup.startsWith("/restapi/")) {
+ return QuotaResponse.noOp();
+ }
+ boolean success = buckets.checkOnly(ctx.user(), numTokens);
+ if (!success) {
+ return QuotaResponse.error("user sent too many qps, please wait for 5 minutes");
+ }
+ return QuotaResponse.ok();
+ }
+
+ @Override
+ void refill(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
+ if (!quotaGroup.startsWith("/restapi/")) {
+ return;
+ }
+ buckets.add(ctx.user(), numTokens);
+ }
+}
+----
+
== SEE ALSO
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 6011158..557cf90 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -85,6 +85,7 @@
.. link:js-api.html[JavaScript Plugin API]
.. link:config-validation.html[Validation Interfaces]
.. link:dev-stars.html[Starring Changes]
+.. link:quota.html[Quota Enforcement]
. link:dev-design.html[System Design]
. link:i18n-readme.html[i18n Support]
diff --git a/Documentation/quota.txt b/Documentation/quota.txt
new file mode 100644
index 0000000..a647e33
--- /dev/null
+++ b/Documentation/quota.txt
@@ -0,0 +1,50 @@
+= Gerrit Code Review - Quota
+
+Gerrit does not provide out of the box quota enforcement. However, it does
+support an extension mechanism for plugins to hook into to provide this
+functionality. The most prominent plugin is the
+link:https://gerrit.googlesource.com/plugins/quota/[Quota Plugin].
+
+This documentation is intended to be read by plugin developers. It contains all
+quota requests implemented in Gerrit-core as well as the metadata that they have
+associated.
+
+== Quota Groups
+
+The following quota groups are defined in core Gerrit:
+
+=== REST API
+[[rest-api]]
+
+The REST API enforces quota after the resource was parsed (if applicable) and before the
+endpoint's logic is executed. This enables quota enforcer implementations to throttle calls
+to specific endpoints while knowing the general context (user and top-level entity such as
+change, project or account).
+
+If the quota enforcer wants to throttle HTTP requests, they should use
+link:quota.html#http-requests[HTTP Requests] instead.
+
+The quota groups used for checking follow the exact definition of the endoint in the REST
+API, but remove all IDs. The schema is:
+
+/restapi/<ENDPOINT>:<HTTP-METHOD>
+
+Examples:
+
+[options="header",cols="1,6"]
+|=======================
+|HTTP call |Quota Group |Metadata
+|GET /a/changes/1/revisions/current/detail |/changes/revisions/detail:GET |CurrentUser, Change.Id, Project.NameKey
+|POST /a/changes/ |/changes/:POST |CurrentUser
+|GET /a/accounts/self/detail |/accounts/detail:GET |CurrentUser, Account.Id
+|=======================
+
+The user provided in the check's metadata is always the calling user (having the
+impersonation bit and real user set in case the user is impersonating another user).
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 8f6a47b..a8ab353 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -191,6 +191,11 @@
"`422 Unprocessable Entity`" is returned if the ID of a resource that is
specified in the request body cannot be resolved.
+==== 429 Too Many Requests
+"`429 Too Many Requests`" is returned if the request exhausted any set
+quota limits. Depending on the exhausted quota, the request may be retried
+with exponential backoff.
+
[[tracing]]
=== Request Tracing
For each REST endpoint tracing can be enabled by setting the
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index b0a39cf..7becf99 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -26,6 +26,7 @@
"//java/com/google/gerrit/pgm/util",
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/audit",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/server/project/testing:project-test-util",
"//java/com/google/gerrit/server/restapi",
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index e74c4b2..2e1b562 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -15,7 +15,7 @@
package com.google.gerrit.httpd;
import com.google.common.cache.Cache;
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.Capable;
@@ -52,6 +52,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
@@ -61,6 +62,7 @@
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.http.server.GitServlet;
import org.eclipse.jgit.http.server.GitSmartHttpTools;
@@ -130,6 +132,55 @@
}
}
+ static class HttpServletResponseWithStatusWrapper extends HttpServletResponseWrapper {
+ private int responseStatus;
+
+ HttpServletResponseWithStatusWrapper(HttpServletResponse response) {
+ super(response);
+ /* Even if we could read the status from response, we assume that it is all
+ * fine because we entered the filter without any prior issues.
+ * When Google will have upgraded to Servlet 3.0, we could actually
+ * call response.getStatus() and the code will be clearer.
+ */
+ responseStatus = HttpServletResponse.SC_OK;
+ }
+
+ @Override
+ public void setStatus(int sc) {
+ responseStatus = sc;
+ super.setStatus(sc);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void setStatus(int sc, String sm) {
+ responseStatus = sc;
+ super.setStatus(sc, sm);
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException {
+ this.responseStatus = sc;
+ super.sendError(sc);
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException {
+ this.responseStatus = sc;
+ super.sendError(sc, msg);
+ }
+
+ @Override
+ public void sendRedirect(String location) throws IOException {
+ this.responseStatus = HttpServletResponse.SC_MOVED_TEMPORARILY;
+ super.sendRedirect(location);
+ }
+
+ public int getResponseStatus() {
+ return responseStatus;
+ }
+ }
+
@Inject
GitOverHttpServlet(
Resolver resolver,
@@ -156,19 +207,15 @@
}
private static ListMultimap<String, String> extractParameters(HttpServletRequest request) {
-
- ListMultimap<String, String> multiMap = ArrayListMultimap.create();
- if (request.getQueryString() != null) {
- request
- .getParameterMap()
- .forEach(
- (k, v) -> {
- for (int i = 0; i < v.length; i++) {
- multiMap.put(k, v[i]);
- }
- });
+ if (request.getQueryString() == null) {
+ return ImmutableListMultimap.of();
}
- return multiMap;
+ // Explicit cast is required to compile under Servlet API 2.5, where the return type is raw Map.
+ @SuppressWarnings("cast")
+ Map<String, String[]> parameterMap = (Map<String, String[]>) request.getParameterMap();
+ ImmutableListMultimap.Builder<String, String> b = ImmutableListMultimap.builder();
+ parameterMap.forEach(b::putAll);
+ return b.build();
}
static class Resolver implements RepositoryResolver<HttpServletRequest> {
@@ -301,41 +348,48 @@
UploadPack up = (UploadPack) request.getAttribute(ServletUtils.ATTRIBUTE_HANDLER);
PermissionBackend.ForProject perm =
permissionBackend.currentUser().project(state.getNameKey());
+ HttpServletResponseWithStatusWrapper responseWrapper =
+ new HttpServletResponseWithStatusWrapper((HttpServletResponse) response);
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ String sessionId = httpRequest.getSession().getId();
+
try {
- perm.check(ProjectPermission.RUN_UPLOAD_PACK);
- } catch (AuthException e) {
- GitSmartHttpTools.sendError(
- (HttpServletRequest) request,
- (HttpServletResponse) response,
- HttpServletResponse.SC_FORBIDDEN,
- "upload-pack not permitted on this server");
- return;
- } catch (PermissionBackendException e) {
- throw new ServletException(e);
+ try {
+ perm.check(ProjectPermission.RUN_UPLOAD_PACK);
+ } catch (AuthException e) {
+ GitSmartHttpTools.sendError(
+ (HttpServletRequest) request,
+ responseWrapper,
+ HttpServletResponse.SC_FORBIDDEN,
+ "upload-pack not permitted on this server");
+ return;
+ } catch (PermissionBackendException e) {
+ responseWrapper.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ throw new ServletException(e);
+ }
+
+ // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
+ // may have been overridden by a proxy server -- we'll try to avoid this.
+ UploadValidators uploadValidators =
+ uploadValidatorsFactory.create(state.getProject(), repo, request.getRemoteHost());
+ up.setPreUploadHook(
+ PreUploadHookChain.newChain(
+ Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
+ up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
+ next.doFilter(httpRequest, responseWrapper);
} finally {
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- HttpServletResponse httpResponse = (HttpServletResponse) response;
groupAuditService.dispatch(
new HttpAuditEvent(
- httpRequest.getSession().getId(),
+ sessionId,
userProvider.get(),
extractWhat(httpRequest),
TimeUtil.nowMs(),
extractParameters(httpRequest),
httpRequest.getMethod(),
httpRequest,
- httpResponse.getStatus(),
- httpResponse));
+ responseWrapper.getResponseStatus(),
+ responseWrapper));
}
-
- // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
- // may have been overridden by a proxy server -- we'll try to avoid this.
- UploadValidators uploadValidators =
- uploadValidatorsFactory.create(state.getProject(), repo, request.getRemoteHost());
- up.setPreUploadHook(
- PreUploadHookChain.newChain(Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
- up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
- next.doFilter(request, response);
}
@Override
@@ -411,25 +465,28 @@
rp.getAdvertiseRefsHook().advertiseRefs(rp);
ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
+ HttpServletResponseWithStatusWrapper responseWrapper =
+ new HttpServletResponseWithStatusWrapper((HttpServletResponse) response);
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
Capable canUpload;
try {
- permissionBackend
- .currentUser()
- .project(state.getNameKey())
- .check(ProjectPermission.RUN_RECEIVE_PACK);
- canUpload = arc.canUpload();
- } catch (AuthException e) {
- GitSmartHttpTools.sendError(
- (HttpServletRequest) request,
- (HttpServletResponse) response,
- HttpServletResponse.SC_FORBIDDEN,
- "receive-pack not permitted on this server");
- return;
- } catch (PermissionBackendException e) {
- throw new RuntimeException(e);
+ try {
+ permissionBackend
+ .currentUser()
+ .project(state.getNameKey())
+ .check(ProjectPermission.RUN_RECEIVE_PACK);
+ canUpload = arc.canUpload();
+ } catch (AuthException e) {
+ GitSmartHttpTools.sendError(
+ httpRequest,
+ responseWrapper,
+ HttpServletResponse.SC_FORBIDDEN,
+ "receive-pack not permitted on this server");
+ return;
+ } catch (PermissionBackendException e) {
+ throw new RuntimeException(e);
+ }
} finally {
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- HttpServletResponse httpResponse = (HttpServletResponse) response;
groupAuditService.dispatch(
new HttpAuditEvent(
httpRequest.getSession().getId(),
@@ -439,26 +496,26 @@
extractParameters(httpRequest),
httpRequest.getMethod(),
httpRequest,
- httpResponse.getStatus(),
- httpResponse));
+ responseWrapper.getResponseStatus(),
+ responseWrapper));
}
if (canUpload != Capable.OK) {
GitSmartHttpTools.sendError(
- (HttpServletRequest) request,
- (HttpServletResponse) response,
+ httpRequest,
+ responseWrapper,
HttpServletResponse.SC_FORBIDDEN,
"\n" + canUpload.getMessage());
return;
}
if (!rp.isCheckReferencedObjectsAreReachable()) {
- chain.doFilter(request, response);
+ chain.doFilter(request, responseWrapper);
return;
}
if (!(userProvider.get().isIdentifiedUser())) {
- chain.doFilter(request, response);
+ chain.doFilter(request, responseWrapper);
return;
}
@@ -475,7 +532,7 @@
}
}
- chain.doFilter(request, response);
+ chain.doFilter(request, responseWrapper);
if (isGet) {
cache.put(cacheKey, Collections.unmodifiableSet(new HashSet<>(rp.getAdvertisedObjects())));
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java b/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
new file mode 100644
index 0000000..c7678c2
--- /dev/null
+++ b/java/com/google/gerrit/httpd/restapi/RestApiQuotaEnforcer.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.restapi;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.quota.QuotaBackend;
+import com.google.gerrit.server.quota.QuotaException;
+import com.google.gerrit.util.http.RequestUtil;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Enforces quota on specific REST API endpoints.
+ *
+ * <p>Examples:
+ *
+ * <ul>
+ * <li>GET /a/accounts/self/detail => /restapi/accounts/detail:GET
+ * <li>GET /changes/123/revisions/current/detail => /restapi/changes/revisions/detail:GET
+ * <li>PUT /changes/10/reviewed => /changes/reviewed:PUT
+ * </ul>
+ *
+ * <p>Adds context (change, project, account) to the quota check if the call is for an existing
+ * entity that was successfully parsed. This quota check is generally enforced after the resource
+ * was parsed, but before the view is executed. If a quota enforcer desires to throttle earlier,
+ * they should consider quota groups in the {@code /http/*} space.
+ */
+public class RestApiQuotaEnforcer {
+ private final QuotaBackend quotaBackend;
+
+ @Inject
+ RestApiQuotaEnforcer(QuotaBackend quotaBackend) {
+ this.quotaBackend = quotaBackend;
+ }
+
+ /** Enforce quota on a request not tied to any {@code RestResource}. */
+ void enforce(HttpServletRequest req) throws QuotaException {
+ String pathForQuotaReporting = RequestUtil.getRestPathWithoutIds(req);
+ quotaBackend
+ .currentUser()
+ .requestToken(quotaGroup(pathForQuotaReporting, req.getMethod()))
+ .throwOnError();
+ }
+
+ /** Enforce quota on a request for a given resource. */
+ void enforce(RestResource rsrc, HttpServletRequest req) throws QuotaException {
+ String pathForQuotaReporting = RequestUtil.getRestPathWithoutIds(req);
+ // Enrich the quota request we are operating on an interesting collection
+ QuotaBackend.WithResource report = quotaBackend.currentUser();
+ if (rsrc instanceof ChangeResource) {
+ ChangeResource changeResource = (ChangeResource) rsrc;
+ report =
+ quotaBackend.currentUser().change(changeResource.getId(), changeResource.getProject());
+ } else if (rsrc instanceof AccountResource) {
+ AccountResource accountResource = (AccountResource) rsrc;
+ report = quotaBackend.currentUser().account(accountResource.getUser().getAccountId());
+ } else if (rsrc instanceof ProjectResource) {
+ ProjectResource accountResource = (ProjectResource) rsrc;
+ report = quotaBackend.currentUser().account(accountResource.getUser().getAccountId());
+ }
+
+ report.requestToken(quotaGroup(pathForQuotaReporting, req.getMethod())).throwOnError();
+ }
+
+ private static String quotaGroup(String path, String method) {
+ return "/restapi" + path + ":" + method;
+ }
+}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index ec71d8f..c9f3a77 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -114,6 +114,7 @@
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.quota.QuotaException;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.util.http.CacheHeaders;
@@ -227,6 +228,7 @@
final AuditService auditService;
final RestApiMetrics metrics;
final Pattern allowOrigin;
+ final RestApiQuotaEnforcer quotaChecker;
@Inject
Globals(
@@ -236,6 +238,7 @@
PermissionBackend permissionBackend,
AuditService auditService,
RestApiMetrics metrics,
+ RestApiQuotaEnforcer quotaChecker,
@GerritServerConfig Config cfg) {
this.currentUser = currentUser;
this.webSession = webSession;
@@ -243,6 +246,7 @@
this.permissionBackend = permissionBackend;
this.auditService = auditService;
this.metrics = metrics;
+ this.quotaChecker = quotaChecker;
allowOrigin = makeAllowOrigin(cfg);
}
@@ -317,6 +321,7 @@
viewData = new ViewData(null, null);
if (path.isEmpty()) {
+ globals.quotaChecker.enforce(req);
if (rc instanceof NeedsParams) {
((NeedsParams) rc).setParams(qp.params());
}
@@ -339,6 +344,7 @@
IdString id = path.remove(0);
try {
rsrc = rc.parse(rsrc, id);
+ globals.quotaChecker.enforce(rsrc, req);
if (path.isEmpty()) {
checkPreconditions(req);
}
@@ -346,6 +352,7 @@
if (!path.isEmpty()) {
throw e;
}
+ globals.quotaChecker.enforce(req);
if (isPost(req) || isPut(req)) {
RestView<RestResource> createView = rc.views().get(PluginName.GERRIT, "CREATE./");
@@ -602,6 +609,9 @@
status = SC_INTERNAL_SERVER_ERROR;
responseBytes = handleException(e, req, res);
}
+ } catch (QuotaException e) {
+ responseBytes =
+ replyError(req, res, status = 429, messageOr(e, "Quota limit reached"), e.caching(), e);
} catch (Exception e) {
status = SC_INTERNAL_SERVER_ERROR;
responseBytes = handleException(e, req, res);
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 8e06aa1..584d8af 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -25,7 +25,6 @@
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerIdProvider;
@@ -37,7 +36,6 @@
import com.google.gerrit.server.group.db.GroupConfig;
import com.google.gerrit.server.group.db.GroupNameNotes;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.File;
import java.io.IOException;
@@ -54,9 +52,8 @@
/**
* A database accessor for calls related to groups.
*
- * <p>All calls which read or write group related details to the database <strong>during
- * init</strong> (either ReviewDb or NoteDb) are gathered here. For non-init cases, use {@code
- * Groups} or {@code GroupsUpdate} instead.
+ * <p>All calls which read or write group related details to the NoteDb <strong>during init</strong>
+ * are gathered here. For non-init cases, use {@code Groups} or {@code GroupsUpdate} instead.
*
* <p>All methods of this class refer to <em>internal</em> groups.
*/
@@ -76,16 +73,14 @@
/**
* Returns the {@code AccountGroup} for the specified {@code GroupReference}.
*
- * @param db the {@code ReviewDb} instance to use for lookups
* @param groupReference the {@code GroupReference} of the group
* @return the {@code InternalGroup} represented by the {@code GroupReference}
- * @throws OrmException if the group couldn't be retrieved from ReviewDb
* @throws IOException if an error occurs while reading from NoteDb
* @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
* @throws NoSuchGroupException if a group with such a name doesn't exist
*/
- public InternalGroup getExistingGroup(ReviewDb db, GroupReference groupReference)
- throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+ public InternalGroup getExistingGroup(GroupReference groupReference)
+ throws NoSuchGroupException, IOException, ConfigInvalidException {
File allUsersRepoPath = getPathToAllUsersRepository();
if (allUsersRepoPath != null) {
try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
@@ -102,14 +97,11 @@
/**
* Returns {@code GroupReference}s for all internal groups.
*
- * @param db the {@code ReviewDb} instance to use for lookups
* @return a stream of the {@code GroupReference}s of all internal groups
- * @throws OrmException if an error occurs while reading from ReviewDb
* @throws IOException if an error occurs while reading from NoteDb
* @throws ConfigInvalidException if the data in NoteDb is in an incorrect format
*/
- public Stream<GroupReference> getAllGroupReferences(ReviewDb db)
- throws OrmException, IOException, ConfigInvalidException {
+ public Stream<GroupReference> getAllGroupReferences() throws IOException, ConfigInvalidException {
File allUsersRepoPath = getPathToAllUsersRepository();
if (allUsersRepoPath != null) {
try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
@@ -126,14 +118,12 @@
* <p><strong>Note</strong>: This method doesn't check whether the account exists! It also doesn't
* update the account index!
*
- * @param db the {@code ReviewDb} instance to update
* @param groupUuid the UUID of the group
* @param account the account to add
- * @throws OrmException if an error occurs while reading/writing from/to ReviewDb
* @throws NoSuchGroupException if the specified group doesn't exist
*/
- public void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, Account account)
- throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+ public void addGroupMember(AccountGroup.UUID groupUuid, Account account)
+ throws NoSuchGroupException, IOException, ConfigInvalidException {
File allUsersRepoPath = getPathToAllUsersRepository();
if (allUsersRepoPath != null) {
try (Repository repository = new FileRepository(allUsersRepoPath)) {
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index f12fa50..f19cf39 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -26,7 +26,6 @@
import com.google.gerrit.pgm.init.api.InitStep;
import com.google.gerrit.pgm.init.api.SequencesOnInit;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountSshKey;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.externalids.ExternalId;
@@ -37,7 +36,6 @@
import com.google.gerrit.server.index.group.GroupIndex;
import com.google.gerrit.server.index.group.GroupIndexCollection;
import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
@@ -57,7 +55,6 @@
private final ExternalIdsOnInit externalIds;
private final SequencesOnInit sequencesOnInit;
private final GroupsOnInit groupsOnInit;
- private SchemaFactory<ReviewDb> dbFactory;
private AccountIndexCollection accountIndexCollection;
private GroupIndexCollection groupIndexCollection;
@@ -84,11 +81,6 @@
@Override
public void run() {}
- @Inject(optional = true)
- void set(SchemaFactory<ReviewDb> dbFactory) {
- this.dbFactory = dbFactory;
- }
-
@Inject
void set(AccountIndexCollection accountIndexCollection) {
this.accountIndexCollection = accountIndexCollection;
@@ -106,58 +98,56 @@
return;
}
- try (ReviewDb db = dbFactory.open()) {
- if (!accounts.hasAnyAccount()) {
- ui.header("Gerrit Administrator");
- if (ui.yesno(true, "Create administrator user")) {
- Account.Id id = new Account.Id(sequencesOnInit.nextAccountId(db));
- String username = ui.readString("admin", "username");
- String name = ui.readString("Administrator", "name");
- String httpPassword = ui.readString("secret", "HTTP password");
- AccountSshKey sshKey = readSshKey(id);
- String email = readEmail(sshKey);
+ if (!accounts.hasAnyAccount()) {
+ ui.header("Gerrit Administrator");
+ if (ui.yesno(true, "Create administrator user")) {
+ Account.Id id = new Account.Id(sequencesOnInit.nextAccountId());
+ String username = ui.readString("admin", "username");
+ String name = ui.readString("Administrator", "name");
+ String httpPassword = ui.readString("secret", "HTTP password");
+ AccountSshKey sshKey = readSshKey(id);
+ String email = readEmail(sshKey);
- List<ExternalId> extIds = new ArrayList<>(2);
- extIds.add(ExternalId.createUsername(username, id, httpPassword));
+ List<ExternalId> extIds = new ArrayList<>(2);
+ extIds.add(ExternalId.createUsername(username, id, httpPassword));
- if (email != null) {
- extIds.add(ExternalId.createEmail(id, email));
- }
- externalIds.insert("Add external IDs for initial admin user", extIds);
+ if (email != null) {
+ extIds.add(ExternalId.createEmail(id, email));
+ }
+ externalIds.insert("Add external IDs for initial admin user", extIds);
- Account a = new Account(id, TimeUtil.nowTs());
- a.setFullName(name);
- a.setPreferredEmail(email);
- accounts.insert(a);
+ Account a = new Account(id, TimeUtil.nowTs());
+ a.setFullName(name);
+ a.setPreferredEmail(email);
+ accounts.insert(a);
- // Only two groups should exist at this point in time and hence iterating over all of them
- // is cheap.
- Optional<GroupReference> adminGroupReference =
- groupsOnInit
- .getAllGroupReferences(db)
- .filter(group -> group.getName().equals("Administrators"))
- .findAny();
- if (!adminGroupReference.isPresent()) {
- throw new NoSuchGroupException("Administrators");
- }
- GroupReference adminGroup = adminGroupReference.get();
- groupsOnInit.addGroupMember(db, adminGroup.getUUID(), a);
+ // Only two groups should exist at this point in time and hence iterating over all of them
+ // is cheap.
+ Optional<GroupReference> adminGroupReference =
+ groupsOnInit
+ .getAllGroupReferences()
+ .filter(group -> group.getName().equals("Administrators"))
+ .findAny();
+ if (!adminGroupReference.isPresent()) {
+ throw new NoSuchGroupException("Administrators");
+ }
+ GroupReference adminGroup = adminGroupReference.get();
+ groupsOnInit.addGroupMember(adminGroup.getUUID(), a);
- if (sshKey != null) {
- VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
- authorizedKeys.addKey(sshKey.sshPublicKey());
- authorizedKeys.save("Add SSH key for initial admin user\n");
- }
+ if (sshKey != null) {
+ VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
+ authorizedKeys.addKey(sshKey.sshPublicKey());
+ authorizedKeys.save("Add SSH key for initial admin user\n");
+ }
- AccountState as = AccountState.forAccount(new AllUsersName(allUsers.get()), a, extIds);
- for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
- accountIndex.replace(as);
- }
+ AccountState as = AccountState.forAccount(new AllUsersName(allUsers.get()), a, extIds);
+ for (AccountIndex accountIndex : accountIndexCollection.getWriteIndexes()) {
+ accountIndex.replace(as);
+ }
- InternalGroup adminInternalGroup = groupsOnInit.getExistingGroup(db, adminGroup);
- for (GroupIndex groupIndex : groupIndexCollection.getWriteIndexes()) {
- groupIndex.replace(adminInternalGroup);
- }
+ InternalGroup adminInternalGroup = groupsOnInit.getExistingGroup(adminGroup);
+ for (GroupIndex groupIndex : groupIndexCollection.getWriteIndexes()) {
+ groupIndex.replace(adminInternalGroup);
}
}
}
diff --git a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
index c9c3a64..1716a3c 100644
--- a/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/SequencesOnInit.java
@@ -35,16 +35,14 @@
this.allUsersName = allUsersName;
}
- public int nextAccountId(ReviewDb db) throws OrmException {
- @SuppressWarnings("deprecation")
- RepoSequence.Seed accountSeed = db::nextAccountId;
+ public int nextAccountId() throws OrmException {
RepoSequence accountSeq =
new RepoSequence(
repoManager,
GitReferenceUpdated.DISABLED,
new Project.NameKey(allUsersName.get()),
Sequences.NAME_ACCOUNTS,
- accountSeed,
+ () -> ReviewDb.FIRST_ACCOUNT_ID,
1);
return accountSeq.next();
}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 8e4292c..90cd066 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -14,8 +14,6 @@
package com.google.gerrit.reviewdb.server;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.Relation;
@@ -28,7 +26,6 @@
* <p>Root entities that are at the top level of some important data graph:
*
* <ul>
- * <li>{@link Account}: Per-user account registration, preferences, identity.
* <li>{@link Change}: All review information about a single proposed change.
* </ul>
*/
@@ -91,22 +88,8 @@
int FIRST_ACCOUNT_ID = 1000000;
- /**
- * Next unique id for a {@link Account}.
- *
- * @deprecated use {@link com.google.gerrit.server.Sequences#nextAccountId()}.
- */
- @Sequence(startWith = FIRST_ACCOUNT_ID)
- @Deprecated
- int nextAccountId() throws OrmException;
-
int FIRST_GROUP_ID = 1;
- /** Next unique id for a {@link AccountGroup}. */
- @Sequence(startWith = FIRST_GROUP_ID)
- @Deprecated
- int nextAccountGroupId() throws OrmException;
-
int FIRST_CHANGE_ID = 1;
/**
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 202729e..c420254 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -136,18 +136,6 @@
@Override
@SuppressWarnings("deprecation")
- public int nextAccountId() throws OrmException {
- return delegate.nextAccountId();
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public int nextAccountGroupId() throws OrmException {
- return delegate.nextAccountGroupId();
- }
-
- @Override
- @SuppressWarnings("deprecation")
public int nextChangeId() throws OrmException {
return delegate.nextChangeId();
}
diff --git a/java/com/google/gerrit/server/Sequences.java b/java/com/google/gerrit/server/Sequences.java
index fcf0759..70a02a8 100644
--- a/java/com/google/gerrit/server/Sequences.java
+++ b/java/com/google/gerrit/server/Sequences.java
@@ -93,11 +93,15 @@
new RepoSequence(
repoManager, gitRefUpdated, allProjects, NAME_CHANGES, changeSeed, changeBatchSize);
- RepoSequence.Seed groupSeed = () -> nextGroupId(db.get());
int groupBatchSize = 1;
groupSeq =
new RepoSequence(
- repoManager, gitRefUpdated, allUsers, NAME_GROUPS, groupSeed, groupBatchSize);
+ repoManager,
+ gitRefUpdated,
+ allUsers,
+ NAME_GROUPS,
+ () -> ReviewDb.FIRST_GROUP_ID,
+ groupBatchSize);
nextIdLatency =
metrics.newTimer(
@@ -158,9 +162,4 @@
private static int nextChangeId(ReviewDb db) throws OrmException {
return db.nextChangeId();
}
-
- @SuppressWarnings("deprecation")
- static int nextGroupId(ReviewDb db) throws OrmException {
- return db.nextAccountGroupId();
- }
}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index b4f9cc7..b26e875 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -170,6 +170,7 @@
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryProcessor;
import com.google.gerrit.server.query.change.ConflictsCacheImpl;
+import com.google.gerrit.server.quota.QuotaEnforcer;
import com.google.gerrit.server.restapi.change.SuggestReviewers;
import com.google.gerrit.server.restapi.group.GroupModule;
import com.google.gerrit.server.rules.DefaultSubmitRule;
@@ -394,6 +395,7 @@
DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
DynamicItem.itemOf(binder(), ProjectNameLockManager.class);
DynamicSet.setOf(binder(), SubmitRule.class);
+ DynamicSet.setOf(binder(), QuotaEnforcer.class);
DynamicMap.mapOf(binder(), MailFilter.class);
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
index be8fcdb..f255ea2 100644
--- a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
@@ -14,10 +14,10 @@
package com.google.gerrit.server.git;
-import com.google.common.collect.ImmutableMap;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import java.io.IOException;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Ref;
@@ -49,13 +49,12 @@
if (prefixes.isEmpty() || prefixes.get(0).isEmpty()) {
refs = repo.getAllRefs();
} else {
- ImmutableMap.Builder<String, Ref> b = new ImmutableMap.Builder<>();
+ refs = new HashMap<>();
for (String prefix : prefixes) {
for (Ref ref : repo.getRefDatabase().getRefsByPrefix(prefix)) {
- b.put(ref.getName(), ref);
+ refs.put(ref.getName(), ref);
}
}
- refs = b.build();
}
return perm.filter(refs, repo, opts);
} catch (IOException | PermissionBackendException e) {
diff --git a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
index bbe0c62..97beefd 100644
--- a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
+++ b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
@@ -22,6 +22,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -33,21 +34,22 @@
/** Helps with the updating of a {@link VersionedMetaData}. */
public class MetaDataUpdate implements AutoCloseable {
+ @Singleton
public static class User {
private final InternalFactory factory;
private final GitRepositoryManager mgr;
- private final PersonIdent serverIdent;
+ private final Provider<PersonIdent> serverIdentProvider;
private final Provider<IdentifiedUser> identifiedUser;
@Inject
User(
InternalFactory factory,
GitRepositoryManager mgr,
- @GerritPersonIdent PersonIdent serverIdent,
+ @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
Provider<IdentifiedUser> identifiedUser) {
this.factory = factory;
this.mgr = mgr;
- this.serverIdent = serverIdent;
+ this.serverIdentProvider = serverIdentProvider;
this.identifiedUser = identifiedUser;
}
@@ -126,29 +128,31 @@
public MetaDataUpdate create(
Project.NameKey name, Repository repository, IdentifiedUser user, BatchRefUpdate batch) {
MetaDataUpdate md = factory.create(name, repository, batch);
- md.getCommitBuilder().setCommitter(serverIdent);
+ md.getCommitBuilder().setCommitter(serverIdentProvider.get());
md.setAuthor(user);
return md;
}
private PersonIdent createPersonIdent(IdentifiedUser user) {
+ PersonIdent serverIdent = serverIdentProvider.get();
return user.newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
}
}
+ @Singleton
public static class Server {
private final InternalFactory factory;
private final GitRepositoryManager mgr;
- private final PersonIdent serverIdent;
+ private final Provider<PersonIdent> serverIdentProvider;
@Inject
Server(
InternalFactory factory,
GitRepositoryManager mgr,
- @GerritPersonIdent PersonIdent serverIdent) {
+ @GerritPersonIdent Provider<PersonIdent> serverIdentProvider) {
this.factory = factory;
this.mgr = mgr;
- this.serverIdent = serverIdent;
+ this.serverIdentProvider = serverIdentProvider;
}
public MetaDataUpdate create(Project.NameKey name)
@@ -162,6 +166,7 @@
Repository repo = mgr.openRepository(name);
MetaDataUpdate md = factory.create(name, repo, batch);
md.setCloseRepository(true);
+ PersonIdent serverIdent = serverIdentProvider.get();
md.getCommitBuilder().setAuthor(serverIdent);
md.getCommitBuilder().setCommitter(serverIdent);
return md;
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index b23fe71..cdfd00d 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -1753,9 +1753,10 @@
reject(cmd, "cannot use merged with base");
return;
}
- RevCommit branchTip = readBranchTip(cmd, magicBranch.dest);
+ RevCommit branchTip = readBranchTip(magicBranch.dest);
if (branchTip == null) {
- return; // readBranchTip already rejected cmd.
+ reject(cmd, magicBranch.dest.get() + " not found");
+ return;
}
if (!walk.isMergedInto(tip, branchTip)) {
reject(cmd, "not merged into branch");
@@ -1793,12 +1794,21 @@
}
}
} else if (newChangeForAllNotInTarget) {
- RevCommit branchTip = readBranchTip(cmd, magicBranch.dest);
- if (branchTip == null) {
- return; // readBranchTip already rejected cmd.
+ RevCommit branchTip = readBranchTip(magicBranch.dest);
+ if (branchTip != null) {
+ magicBranch.baseCommit = Collections.singletonList(branchTip);
+ logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
+ } else {
+ // The target branch does not exist. Usually pushing changes for review requires that the
+ // target branch exists, but there is an exception for the branch to which HEAD points to
+ // and for refs/meta/config. Pushing for review to these branches is allowed even if the
+ // branch does not exist yet. This allows to push initial code for review to an empty
+ // repository and to review an initial project configuration.
+ if (!ref.equals(readHEAD(repo)) && !ref.equals(RefNames.REFS_CONFIG)) {
+ reject(cmd, magicBranch.dest.get() + " not found");
+ return;
+ }
}
- magicBranch.baseCommit = Collections.singletonList(branchTip);
- logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
}
} catch (IOException ex) {
logger.atWarning().withCause(ex).log(
@@ -1872,10 +1882,9 @@
}
}
- private RevCommit readBranchTip(ReceiveCommand cmd, Branch.NameKey branch) throws IOException {
+ private RevCommit readBranchTip(Branch.NameKey branch) throws IOException {
Ref r = allRefs().get(branch.get());
if (r == null) {
- reject(cmd, branch.get() + " not found");
return null;
}
return receivePack.getRevWalk().parseCommit(r.getObjectId());
@@ -2727,10 +2736,13 @@
/** prints a warning if the new PS has the same tree as the previous commit. */
private void sameTreeWarning() throws IOException {
- RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+ RevWalk rw = receivePack.getRevWalk();
+ RevCommit newCommit = rw.parseCommit(newCommitId);
RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
if (newCommit.getTree().equals(priorCommit.getTree())) {
+ rw.parseBody(newCommit);
+ rw.parseBody(priorCommit);
boolean messageEq =
Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
boolean parentsEq = parentsEqual(newCommit, priorCommit);
diff --git a/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
new file mode 100644
index 0000000..8aa86b1
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/DefaultQuotaBackend.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2018 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.quota;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.plugincontext.PluginSetEntryContext;
+import java.util.ArrayList;
+import java.util.List;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+@Singleton
+public class DefaultQuotaBackend implements QuotaBackend {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Provider<CurrentUser> userProvider;
+ private final PluginSetContext<QuotaEnforcer> quotaEnforcers;
+
+ @Inject
+ DefaultQuotaBackend(
+ Provider<CurrentUser> userProvider, PluginSetContext<QuotaEnforcer> quotaEnforcers) {
+ this.userProvider = userProvider;
+ this.quotaEnforcers = quotaEnforcers;
+ }
+
+ @Override
+ public WithUser currentUser() {
+ return new WithUser(quotaEnforcers, userProvider.get());
+ }
+
+ @Override
+ public WithUser user(CurrentUser user) {
+ return new WithUser(quotaEnforcers, user);
+ }
+
+ private static QuotaResponse.Aggregated request(
+ PluginSetContext<QuotaEnforcer> quotaEnforcers,
+ String quotaGroup,
+ QuotaRequestContext requestContext,
+ long numTokens,
+ boolean deduct) {
+ checkState(numTokens > 0, "numTokens must be a positive, non-zero long");
+
+ // PluginSets can change their content when plugins (de-)register. Copy the currently registered
+ // plugins so that we can iterate twice on a stable list.
+ List<PluginSetEntryContext<QuotaEnforcer>> enforcers = ImmutableList.copyOf(quotaEnforcers);
+ List<QuotaResponse> responses = new ArrayList<>(enforcers.size());
+ for (PluginSetEntryContext<QuotaEnforcer> enforcer : enforcers) {
+ try {
+ if (deduct) {
+ responses.add(enforcer.call(p -> p.requestTokens(quotaGroup, requestContext, numTokens)));
+ } else {
+ responses.add(enforcer.call(p -> p.dryRun(quotaGroup, requestContext, numTokens)));
+ }
+ } catch (RuntimeException e) {
+ logger.atSevere().withCause(e).log("exception while enforcing quota");
+ responses.add(QuotaResponse.error(e.getMessage()));
+ }
+ }
+
+ if (deduct && responses.stream().anyMatch(r -> r.status().isError())) {
+ // Roll back the quota request for all enforcers that deducted the quota (= the request
+ // succeeded). Don't touch failed enforcers as the interface contract said that failed
+ // requests should not be deducted.
+ for (int i = 0; i < responses.size(); i++) {
+ if (responses.get(i).status().isOk()) {
+ enforcers.get(i).run(p -> p.refill(quotaGroup, requestContext, numTokens));
+ }
+ }
+ }
+
+ logger.atFine().log(
+ "Quota request for %s with %s (deduction=%s) for %s token returned %s",
+ quotaGroup,
+ requestContext,
+ deduct ? "(deduction=yes)" : "(deduction=no)",
+ numTokens,
+ responses);
+ return new AutoValue_QuotaResponse_Aggregated(ImmutableList.copyOf(responses));
+ }
+
+ static class WithUser extends WithResource implements QuotaBackend.WithUser {
+ WithUser(PluginSetContext<QuotaEnforcer> quotaEnforcers, CurrentUser user) {
+ super(quotaEnforcers, QuotaRequestContext.builder().user(user).build());
+ }
+
+ @Override
+ public QuotaBackend.WithResource account(Account.Id account) {
+ QuotaRequestContext ctx = requestContext.toBuilder().account(account).build();
+ return new WithResource(quotaEnforcers, ctx);
+ }
+
+ @Override
+ public QuotaBackend.WithResource project(NameKey project) {
+ QuotaRequestContext ctx = requestContext.toBuilder().project(project).build();
+ return new WithResource(quotaEnforcers, ctx);
+ }
+
+ @Override
+ public QuotaBackend.WithResource change(Change.Id change, NameKey project) {
+ QuotaRequestContext ctx = requestContext.toBuilder().change(change).project(project).build();
+ return new WithResource(quotaEnforcers, ctx);
+ }
+ }
+
+ static class WithResource implements QuotaBackend.WithResource {
+ protected final QuotaRequestContext requestContext;
+ protected final PluginSetContext<QuotaEnforcer> quotaEnforcers;
+
+ private WithResource(
+ PluginSetContext<QuotaEnforcer> quotaEnforcers, QuotaRequestContext quotaRequestContext) {
+ this.quotaEnforcers = quotaEnforcers;
+ this.requestContext = quotaRequestContext;
+ }
+
+ @Override
+ public QuotaResponse.Aggregated requestTokens(String quotaGroup, long numTokens) {
+ return DefaultQuotaBackend.request(
+ quotaEnforcers, quotaGroup, requestContext, numTokens, true);
+ }
+
+ @Override
+ public QuotaResponse.Aggregated dryRun(String quotaGroup, long numTokens) {
+ return DefaultQuotaBackend.request(
+ quotaEnforcers, quotaGroup, requestContext, numTokens, false);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaBackend.java b/java/com/google/gerrit/server/quota/QuotaBackend.java
new file mode 100644
index 0000000..d4dc46d
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaBackend.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2018 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.quota;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.ImplementedBy;
+
+/**
+ * Backend interface to perform quota requests on. By default, this interface is backed by {@link
+ * DefaultQuotaBackend} which calls all plugins that implement {@link QuotaEnforcer}. A different
+ * implementation might be bound in tests. Plugins are not supposed to implement this interface, but
+ * bind a {@link QuotaEnforcer} implementation instead.
+ *
+ * <p>All quota requests require a quota group and a user. Enriching them with a top-level entity
+ * {@code Change, Project, Account} is optional but should be done if the request is targeted.
+ *
+ * <p>Example usage:
+ *
+ * <pre>
+ * quotaBackend.currentUser().project(projectName).requestToken("/projects/create").throwOnError();
+ * quotaBackend.user(user).requestToken("/restapi/config/put").throwOnError();
+ * QuotaResponse.Aggregated result = quotaBackend.currentUser().account(accountId).requestToken("/restapi/accounts/emails/validate");
+ * QuotaResponse.Aggregated result = quotaBackend.currentUser().project(projectName).requestTokens("/projects/git/upload", numBytesInPush);
+ * </pre>
+ *
+ * <p>All quota groups must be documented in {@code quota.txt} and detail the metadata that is
+ * provided (i.e. the parameters used to scope down the quota request).
+ */
+@ImplementedBy(DefaultQuotaBackend.class)
+public interface QuotaBackend {
+ /** Constructs a request for the current user. */
+ WithUser currentUser();
+
+ /**
+ * See {@link #currentUser()}. Use this method only if you can't guarantee that the request is for
+ * the current user (e.g. impersonation).
+ */
+ WithUser user(CurrentUser user);
+
+ /**
+ * An interface capable of issuing quota requests. Scope can be futher reduced by providing a
+ * top-level entity.
+ */
+ interface WithUser extends WithResource {
+ /** Scope the request down to an account. */
+ WithResource account(Account.Id account);
+
+ /** Scope the request down to a project. */
+ WithResource project(Project.NameKey project);
+
+ /** Scope the request down to a change. */
+ WithResource change(Change.Id change, Project.NameKey project);
+ }
+
+ /** An interface capable of issuing quota requests. */
+ interface WithResource {
+ /** Issues a single quota request for {@code 1} token. */
+ default QuotaResponse.Aggregated requestToken(String quotaGroup) {
+ return requestTokens(quotaGroup, 1);
+ }
+
+ /** Issues a single quota request for {@code numTokens} tokens. */
+ QuotaResponse.Aggregated requestTokens(String quotaGroup, long numTokens);
+
+ /**
+ * Issues a single quota request for {@code numTokens} tokens but signals the implementations
+ * not to deduct any quota yet. Can be used to do pre-flight requests where necessary
+ */
+ QuotaResponse.Aggregated dryRun(String quotaGroup, long tokens);
+ }
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaEnforcer.java b/java/com/google/gerrit/server/quota/QuotaEnforcer.java
new file mode 100644
index 0000000..9c55e11
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaEnforcer.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2018 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.quota;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Allows plugins to enforce different types of quota.
+ *
+ * <p>Enforcing quotas can be helpful in many scenarios. For example:
+ *
+ * <ul>
+ * <li>Reducing the number of QPS a user can send to Gerrit on the REST API
+ * <li>Limiting the size of a repository (project)
+ * <li>Limiting the number of changes in a repository
+ * <li>Limiting the number of actions that have the potential for spam, abuse or flooding if not
+ * limited
+ * </ul>
+ *
+ * This endpoint gives plugins the capability to enforce any of these limits. The server will ask
+ * all plugins that registered this endpoint and collect all results. In case {@link
+ * #requestTokens(String, QuotaRequestContext, long)} was called and one or more plugins returned an
+ * erroneous result, the server will call {@link #refill(String, QuotaRequestContext, long)} on all
+ * plugins with the same parameters. Plugins that deducted tokens in the {@link
+ * #requestTokens(String, QuotaRequestContext, long)} call can refill them so that users don't get
+ * charged any quota for failed requests.
+ *
+ * <p>Not all implementations will need to deduct quota on {@link #requestTokens(String,
+ * QuotaRequestContext, long)}}. Implementations that work on top of instance-attributes, such as
+ * the number of projects per instance can choose not to keep any state and always check how many
+ * existing projects there are and if adding the inquired number would exceed the limit. In this
+ * case, {@link #requestTokens(String, QuotaRequestContext, long)} and {@link #dryRun(String,
+ * QuotaRequestContext, long)} share the same implementation and {@link #refill(String,
+ * QuotaRequestContext, long)} is a no-op.
+ */
+@ExtensionPoint
+public interface QuotaEnforcer {
+ /**
+ * Checks if there is at least {@code numTokens} quota to fulfil the request. Bucket-based
+ * implementations can deduct the inquired number of tokens from the bucket.
+ */
+ QuotaResponse requestTokens(String quotaGroup, QuotaRequestContext ctx, long numTokens);
+
+ /**
+ * Checks if there is at least {@code numTokens} quota to fulfil the request. This is a pre-flight
+ * request, implementations should not deduct tokens from a bucket, yet.
+ */
+ QuotaResponse dryRun(String quotaGroup, QuotaRequestContext ctx, long numTokens);
+
+ /**
+ * A previously requested and deducted quota has to be refilled (if possible) because the request
+ * failed other quota checks. Implementations can choose to leave this a no-op in case they are
+ * the first line of defence (e.g. always deduct HTTP quota even if the request failed for other
+ * quota issues so that the user gets throttled).
+ *
+ * <p>Will not be called if the {@link #requestTokens(String, QuotaRequestContext, long)} call
+ * returned {@link QuotaResponse.Status#NO_OP}.
+ */
+ void refill(String quotaGroup, QuotaRequestContext ctx, long numTokens);
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaException.java b/java/com/google/gerrit/server/quota/QuotaException.java
new file mode 100644
index 0000000..56877b2
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaException.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 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.quota;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+/**
+ * Exception that was encountered while checking if there is sufficient quota to fulfil the request.
+ * Can be propagated directly to the REST API.
+ */
+public class QuotaException extends RestApiException {
+ public QuotaException(String reason) {
+ super(reason);
+ }
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaRequestContext.java b/java/com/google/gerrit/server/quota/QuotaRequestContext.java
new file mode 100644
index 0000000..90b501c
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaRequestContext.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2018 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.quota;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import java.util.Optional;
+
+@AutoValue
+public abstract class QuotaRequestContext {
+
+ public static Builder builder() {
+ return new AutoValue_QuotaRequestContext.Builder().user(new AnonymousUser());
+ }
+
+ public abstract CurrentUser user();
+
+ public abstract Optional<Project.NameKey> project();
+
+ public abstract Optional<Change.Id> change();
+
+ public abstract Optional<Account.Id> account();
+
+ public abstract Builder toBuilder();
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract QuotaRequestContext.Builder user(CurrentUser user);
+
+ public abstract QuotaRequestContext.Builder account(Account.Id account);
+
+ public abstract QuotaRequestContext.Builder project(Project.NameKey project);
+
+ public abstract QuotaRequestContext.Builder change(Change.Id change);
+
+ public abstract QuotaRequestContext build();
+ }
+}
diff --git a/java/com/google/gerrit/server/quota/QuotaResponse.java b/java/com/google/gerrit/server/quota/QuotaResponse.java
new file mode 100644
index 0000000..c239aaf
--- /dev/null
+++ b/java/com/google/gerrit/server/quota/QuotaResponse.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2018 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.quota;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@AutoValue
+public abstract class QuotaResponse {
+ public enum Status {
+ /** The quota requests succeeded. */
+ OK,
+
+ /**
+ * The quota succeeded, but was a no-op because the plugin does not enforce this quota group
+ * (equivalent to OK, but relevant for debugging).
+ */
+ NO_OP,
+
+ /**
+ * The requested quota could not be allocated. This status code is not used to indicate
+ * processing failures as these are propagated as {@code RuntimeException}s.
+ */
+ ERROR;
+
+ public boolean isOk() {
+ return this == OK;
+ }
+
+ public boolean isError() {
+ return this == ERROR;
+ }
+ }
+
+ public static QuotaResponse ok() {
+ return new AutoValue_QuotaResponse.Builder().status(Status.OK).build();
+ }
+
+ public static QuotaResponse noOp() {
+ return new AutoValue_QuotaResponse.Builder().status(Status.NO_OP).build();
+ }
+
+ public static QuotaResponse error(String message) {
+ return new AutoValue_QuotaResponse.Builder().status(Status.ERROR).message(message).build();
+ }
+
+ public abstract Status status();
+
+ public abstract Optional<String> message();
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract QuotaResponse.Builder status(Status status);
+
+ public abstract QuotaResponse.Builder message(String message);
+
+ public abstract QuotaResponse build();
+ }
+
+ @AutoValue
+ public abstract static class Aggregated {
+ protected abstract ImmutableList<QuotaResponse> responses();
+
+ public boolean hasError() {
+ return responses().stream().anyMatch(r -> r.status().isError());
+ }
+
+ public ImmutableList<QuotaResponse> all() {
+ return responses();
+ }
+
+ public ImmutableList<QuotaResponse> ok() {
+ return responses().stream().filter(r -> r.status().isOk()).collect(toImmutableList());
+ }
+
+ public ImmutableList<QuotaResponse> error() {
+ return responses().stream().filter(r -> r.status().isError()).collect(toImmutableList());
+ }
+
+ public String errorMessage() {
+ return error()
+ .stream()
+ .map(QuotaResponse::message)
+ .flatMap(Streams::stream)
+ .collect(Collectors.joining(", "));
+ }
+
+ public void throwOnError() throws QuotaException {
+ String errorMessage = errorMessage();
+ if (!Strings.isNullOrEmpty(errorMessage)) {
+ throw new QuotaException(errorMessage);
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/schema/Schema_155.java b/java/com/google/gerrit/server/schema/Schema_155.java
index 812d7a6..e9372a5 100644
--- a/java/com/google/gerrit/server/schema/Schema_155.java
+++ b/java/com/google/gerrit/server/schema/Schema_155.java
@@ -40,15 +40,13 @@
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
- @SuppressWarnings("deprecation")
- RepoSequence.Seed accountSeed = db::nextAccountId;
RepoSequence accountSeq =
new RepoSequence(
repoManager,
GitReferenceUpdated.DISABLED,
allUsersName,
Sequences.NAME_ACCOUNTS,
- accountSeed,
+ () -> ReviewDb.FIRST_ACCOUNT_ID,
1);
// consume one account ID to ensure that the account sequence is initialized in NoteDb
diff --git a/java/com/google/gerrit/server/schema/Schema_163.java b/java/com/google/gerrit/server/schema/Schema_163.java
index 9eb5d5e..4b3659de 100644
--- a/java/com/google/gerrit/server/schema/Schema_163.java
+++ b/java/com/google/gerrit/server/schema/Schema_163.java
@@ -40,15 +40,13 @@
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
- @SuppressWarnings("deprecation")
- RepoSequence.Seed groupSeed = db::nextAccountGroupId;
RepoSequence groupSeq =
new RepoSequence(
repoManager,
GitReferenceUpdated.DISABLED,
allUsersName,
Sequences.NAME_GROUPS,
- groupSeed,
+ () -> ReviewDb.FIRST_GROUP_ID,
1);
// consume one account ID to ensure that the group sequence is initialized in NoteDb
diff --git a/java/com/google/gerrit/testing/DisabledReviewDb.java b/java/com/google/gerrit/testing/DisabledReviewDb.java
index 2bf95b0..d06beb9 100644
--- a/java/com/google/gerrit/testing/DisabledReviewDb.java
+++ b/java/com/google/gerrit/testing/DisabledReviewDb.java
@@ -95,16 +95,6 @@
}
@Override
- public int nextAccountId() {
- throw new Disabled();
- }
-
- @Override
- public int nextAccountGroupId() {
- throw new Disabled();
- }
-
- @Override
public int nextChangeId() {
throw new Disabled();
}
diff --git a/java/com/google/gerrit/testing/FakeGroupAuditService.java b/java/com/google/gerrit/testing/FakeGroupAuditService.java
index f2e85cd..ddf03f5 100644
--- a/java/com/google/gerrit/testing/FakeGroupAuditService.java
+++ b/java/com/google/gerrit/testing/FakeGroupAuditService.java
@@ -63,7 +63,10 @@
@Override
public void dispatch(AuditEvent action) {
- auditEvents.add(action);
+ synchronized (auditEvents) {
+ auditEvents.add(action);
+ auditEvents.notifyAll();
+ }
}
@Override
diff --git a/java/com/google/gerrit/util/http/RequestUtil.java b/java/com/google/gerrit/util/http/RequestUtil.java
index 2a359ca..2543ada 100644
--- a/java/com/google/gerrit/util/http/RequestUtil.java
+++ b/java/com/google/gerrit/util/http/RequestUtil.java
@@ -56,5 +56,38 @@
return pathInfo;
}
+ /**
+ * Trims leading '/' and 'a/'. Removes the context path, but keeps the servlet path. Removes all
+ * IDs from the rest of the URI.
+ *
+ * <p>The returned string is a good fit for cases where one wants the full context of the request
+ * without any identifiable data. For example: Logging or quota checks.
+ *
+ * <p>Examples:
+ *
+ * <ul>
+ * <li>/a/accounts/self/detail => /accounts/detail
+ * <li>/changes/123/revisions/current/detail => /changes/revisions/detail
+ * <li>/changes/ => /changes
+ * </ul>
+ */
+ public static String getRestPathWithoutIds(HttpServletRequest req) {
+ String encodedPathInfo = req.getRequestURI().substring(req.getContextPath().length());
+ if (encodedPathInfo.startsWith("/")) {
+ encodedPathInfo = encodedPathInfo.substring(1);
+ }
+ if (encodedPathInfo.startsWith("a/")) {
+ encodedPathInfo = encodedPathInfo.substring(2);
+ }
+
+ String[] parts = encodedPathInfo.split("/");
+ StringBuilder result = new StringBuilder(parts.length);
+ for (int i = 0; i < parts.length; i = i + 2) {
+ result.append("/");
+ result.append(parts[i]);
+ }
+ return result.toString();
+ }
+
private RequestUtil() {}
}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 6f25d28..18eb37a 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -89,14 +89,13 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.sql.Timestamp;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -334,9 +333,6 @@
String p = createUniqueGroup();
String g1 = createUniqueGroup();
String g2 = createUniqueGroup();
- List<String> groups = new ArrayList<>();
- groups.add(g1);
- groups.add(g2);
gApi.groups().id(p).addGroups(g1, g2);
assertIncludes(p, g1, g2);
}
@@ -952,8 +948,8 @@
}
/**
- * @Sandboxed is used by this test because it deletes a group reference which introduces an
- * inconsistency for the group storage. Once group deletion is supported, this test should be
+ * {@code @Sandboxed} is used by this test because it deletes a group reference which introduces
+ * an inconsistency for the group storage. Once group deletion is supported, this test should be
* updated to use the API instead.
*/
@Test
@@ -1001,8 +997,7 @@
TestAccount groupOwner = accountCreator.user2();
GroupInput in = new GroupInput();
in.name = name("group");
- in.members =
- Collections.singleton(groupOwner).stream().map(u -> u.id.toString()).collect(toList());
+ in.members = Stream.of(groupOwner).map(u -> u.id.toString()).collect(toList());
in.visibleToAll = true;
GroupInfo group = gApi.groups().create(in).get();
@@ -1045,7 +1040,7 @@
@Test
public void pushToGroupsBranchForNonAllUsersRepo() throws Exception {
- assertCreateGroupBranch(project, null);
+ assertCreateGroupBranch(project);
String groupRef =
RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
createBranch(project, groupRef);
@@ -1054,7 +1049,7 @@
@Test
public void pushToDeletedGroupsBranchForNonAllUsersRepo() throws Exception {
- assertCreateGroupBranch(project, null);
+ assertCreateGroupBranch(project);
String groupRef =
RefNames.refsDeletedGroups(
new AccountGroup.UUID(gApi.groups().create(name("foo")).get().id));
@@ -1092,8 +1087,7 @@
}
}
- private void assertCreateGroupBranch(Project.NameKey project, String expectedErrorOnCreate)
- throws Exception {
+ private void assertCreateGroupBranch(Project.NameKey project) throws Exception {
grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
TestRepository<InMemoryRepository> repo = cloneProject(project);
@@ -1102,11 +1096,7 @@
.create(db, admin.getIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
.setParents(ImmutableList.of())
.to(RefNames.REFS_GROUPS + name("bar"));
- if (expectedErrorOnCreate != null) {
- r.assertErrorStatus(expectedErrorOnCreate);
- } else {
- r.assertOkStatus();
- }
+ r.assertOkStatus();
}
@Test
@@ -1495,7 +1485,7 @@
private void assertMembers(String group, TestAccount... expectedMembers) throws Exception {
assertMembers(
gApi.groups().id(group).members(),
- TestAccount.names(expectedMembers).stream().toArray(String[]::new));
+ TestAccount.names(expectedMembers).toArray(new String[0]));
assertAccountInfos(Arrays.asList(expectedMembers), gApi.groups().id(group).members());
}
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 228784b..7529ee3 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -221,6 +221,18 @@
@Test
@TestProjectInput(createEmptyCommit = false)
public void pushInitialCommitSeriesForMasterBranch() throws Exception {
+ testPushInitialCommitSeriesForMasterBranch();
+ }
+
+ @Test
+ @TestProjectInput(createEmptyCommit = false)
+ public void pushInitialCommitSeriesForMasterBranchWithCreateNewChangeForAllNotInTarget()
+ throws Exception {
+ enableCreateNewChangeForAllNotInTarget();
+ testPushInitialCommitSeriesForMasterBranch();
+ }
+
+ private void testPushInitialCommitSeriesForMasterBranch() throws Exception {
RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create();
String id = GitUtil.getChangeId(testRepo, c).get();
testRepo.reset(c);
@@ -1464,14 +1476,7 @@
@Test
public void pushSameCommitTwice() throws Exception {
- try (ProjectConfigUpdate u = updateProject(project)) {
- u.getConfig()
- .getProject()
- .setBooleanConfig(
- BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
- InheritableBoolean.TRUE);
- u.save();
- }
+ enableCreateNewChangeForAllNotInTarget();
PushOneCommit push =
pushFactory.create(
@@ -1493,14 +1498,7 @@
@Test
public void pushSameCommitTwiceWhenIndexFailed() throws Exception {
- try (ProjectConfigUpdate u = updateProject(project)) {
- u.getConfig()
- .getProject()
- .setBooleanConfig(
- BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
- InheritableBoolean.TRUE);
- u.save();
- }
+ enableCreateNewChangeForAllNotInTarget();
PushOneCommit push =
pushFactory.create(
@@ -2356,6 +2354,113 @@
assertPushOk(pr, "refs/heads/permitted");
}
+ @Test
+ public void pushCommitsWithSameTreeNoChanges() throws Exception {
+ RevCommit c =
+ testRepo
+ .commit()
+ .message("Foo")
+ .parent(getHead(testRepo.getRepository()))
+ .insertChangeId()
+ .create();
+ testRepo.reset(c);
+
+ String r = "refs/for/master";
+ PushResult pr = pushHead(testRepo, r, false);
+ assertPushOk(pr, r);
+
+ RevCommit amended = testRepo.amend(c).create();
+ testRepo.reset(amended);
+
+ pr = pushHead(testRepo, r, false);
+ assertPushOk(pr, r);
+ assertThat(pr.getMessages())
+ .contains(
+ "warning: no changes between prior commit "
+ + c.abbreviate(7).name()
+ + " and new commit "
+ + amended.abbreviate(7).name());
+ }
+
+ @Test
+ public void pushCommitsWithSameTreeNoFilesChangedMessageUpdated() throws Exception {
+ RevCommit c =
+ testRepo
+ .commit()
+ .message("Foo")
+ .parent(getHead(testRepo.getRepository()))
+ .insertChangeId()
+ .create();
+ String id = GitUtil.getChangeId(testRepo, c).get();
+ testRepo.reset(c);
+
+ String r = "refs/for/master";
+ PushResult pr = pushHead(testRepo, r, false);
+ assertPushOk(pr, r);
+
+ RevCommit amended =
+ testRepo.amend(c).message("Foo Bar").insertChangeId(id.substring(1)).create();
+ testRepo.reset(amended);
+
+ pr = pushHead(testRepo, r, false);
+ assertPushOk(pr, r);
+ assertThat(pr.getMessages())
+ .contains(
+ "warning: " + amended.abbreviate(7).name() + ": no files changed, message updated");
+ }
+
+ @Test
+ public void pushCommitsWithSameTreeNoFilesChangedAuthorChanged() throws Exception {
+ RevCommit c =
+ testRepo
+ .commit()
+ .message("Foo")
+ .parent(getHead(testRepo.getRepository()))
+ .insertChangeId()
+ .create();
+ testRepo.reset(c);
+
+ String r = "refs/for/master";
+ PushResult pr = pushHead(testRepo, r, false);
+ assertPushOk(pr, r);
+
+ RevCommit amended = testRepo.amend(c).author(user.getIdent()).create();
+ testRepo.reset(amended);
+
+ pr = pushHead(testRepo, r, false);
+ assertPushOk(pr, r);
+ assertThat(pr.getMessages())
+ .contains(
+ "warning: " + amended.abbreviate(7).name() + ": no files changed, author changed");
+ }
+
+ @Test
+ public void pushCommitsWithSameTreeNoFilesChangedWasRebased() throws Exception {
+ RevCommit head = getHead(testRepo.getRepository());
+ RevCommit c = testRepo.commit().message("Foo").parent(head).insertChangeId().create();
+ testRepo.reset(c);
+
+ String r = "refs/for/master";
+ PushResult pr = pushHead(testRepo, r, false);
+ assertPushOk(pr, r);
+
+ testRepo.reset(head);
+ RevCommit newBase = testRepo.commit().message("Base").parent(head).insertChangeId().create();
+ testRepo.reset(newBase);
+
+ pr = pushHead(testRepo, r, false);
+ assertPushOk(pr, r);
+
+ testRepo.reset(c);
+ RevCommit amended = testRepo.amend(c).parent(newBase).create();
+ testRepo.reset(amended);
+
+ pr = pushHead(testRepo, r, false);
+ assertPushOk(pr, r);
+ assertThat(pr.getMessages())
+ .contains("warning: " + amended.abbreviate(7).name() + ": no files changed, was rebased");
+ }
+
private DraftInput newDraft(String path, int line, String message) {
DraftInput d = new DraftInput();
d.path = path;
diff --git a/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java b/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
index 42e046a..90f4134 100644
--- a/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
@@ -17,7 +17,9 @@
import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.server.AuditEvent;
+import com.google.gerrit.server.audit.HttpAuditEvent;
import java.util.Collections;
+import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
@@ -25,6 +27,7 @@
import org.junit.Test;
public class GitOverHttpServletIT extends AbstractPushForReview {
+ private static final long AUDIT_EVENT_TIMEOUT = 500L;
@Before
public void beforeEach() throws Exception {
@@ -42,6 +45,7 @@
.setRemote("origin")
.setRefSpecs(new RefSpec("HEAD:refs/for/master"))
.call();
+ waitForAudit();
// Git smart protocol makes two requests:
// https://github.com/git/git/blob/master/Documentation/technical/http-protocol.txt
@@ -51,11 +55,13 @@
assertThat(e.who.getAccountId()).isEqualTo(admin.id);
assertThat(e.what).endsWith("/git-receive-pack");
assertThat(e.params).isEmpty();
+ assertThat(((HttpAuditEvent) e).httpStatus).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void uploadPackAuditEventLog() throws Exception {
testRepo.git().fetch().call();
+ waitForAudit();
assertThat(auditService.auditEvents.size()).isEqualTo(1);
@@ -64,5 +70,12 @@
assertThat(e.params.get("service"))
.containsExactlyElementsIn(Collections.singletonList("git-upload-pack"));
assertThat(e.what).endsWith("service=git-upload-pack");
+ assertThat(((HttpAuditEvent) e).httpStatus).isEqualTo(HttpServletResponse.SC_OK);
+ }
+
+ private void waitForAudit() throws InterruptedException {
+ synchronized (auditService.auditEvents) {
+ auditService.auditEvents.wait(AUDIT_EVENT_TIMEOUT);
+ }
}
}
diff --git a/javatests/com/google/gerrit/util/http/RequestUtilTest.java b/javatests/com/google/gerrit/util/http/RequestUtilTest.java
index 0bf34e7..adda5e7 100644
--- a/javatests/com/google/gerrit/util/http/RequestUtilTest.java
+++ b/javatests/com/google/gerrit/util/http/RequestUtilTest.java
@@ -15,6 +15,8 @@
package com.google.gerrit.util.http;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.util.http.RequestUtil.getEncodedPathInfo;
+import static com.google.gerrit.util.http.RequestUtil.getRestPathWithoutIds;
import com.google.gerrit.testing.GerritBaseTests;
import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
@@ -22,36 +24,45 @@
public class RequestUtilTest extends GerritBaseTests {
@Test
- public void emptyContextPath() {
- assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("", "/s", "/foo/bar")))
- .isEqualTo("/foo/bar");
- assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("", "/s", "/foo%2Fbar")))
- .isEqualTo("/foo%2Fbar");
+ public void getEncodedPathInfo_emptyContextPath() {
+ assertThat(getEncodedPathInfo(fakeRequest("", "/s", "/foo/bar"))).isEqualTo("/foo/bar");
+ assertThat(getEncodedPathInfo(fakeRequest("", "/s", "/foo%2Fbar"))).isEqualTo("/foo%2Fbar");
}
@Test
- public void emptyServletPath() {
- assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("", "/c", "/foo/bar")))
- .isEqualTo("/foo/bar");
- assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("", "/c", "/foo%2Fbar")))
- .isEqualTo("/foo%2Fbar");
+ public void getEncodedPathInfo_emptyServletPath() {
+ assertThat(getEncodedPathInfo(fakeRequest("", "/c", "/foo/bar"))).isEqualTo("/foo/bar");
+ assertThat(getEncodedPathInfo(fakeRequest("", "/c", "/foo%2Fbar"))).isEqualTo("/foo%2Fbar");
}
@Test
- public void trailingSlashes() {
- assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", "/foo/bar/")))
- .isEqualTo("/foo/bar/");
- assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", "/foo/bar///")))
- .isEqualTo("/foo/bar/");
- assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", "/foo%2Fbar/")))
- .isEqualTo("/foo%2Fbar/");
- assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", "/foo%2Fbar///")))
+ public void getEncodedPathInfo_trailingSlashes() {
+ assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", "/foo/bar/"))).isEqualTo("/foo/bar/");
+ assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", "/foo/bar///"))).isEqualTo("/foo/bar/");
+ assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", "/foo%2Fbar/"))).isEqualTo("/foo%2Fbar/");
+ assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", "/foo%2Fbar///")))
.isEqualTo("/foo%2Fbar/");
}
@Test
public void emptyPathInfo() {
- assertThat(RequestUtil.getEncodedPathInfo(fakeRequest("/c", "/s", ""))).isNull();
+ assertThat(getEncodedPathInfo(fakeRequest("/c", "/s", ""))).isNull();
+ }
+
+ @Test
+ public void getRestPathWithoutIds_emptyContextPath() {
+ assertThat(getRestPathWithoutIds(fakeRequest("", "/a/accounts", "/123/test")))
+ .isEqualTo("/accounts/test");
+ assertThat(getRestPathWithoutIds(fakeRequest("", "/accounts", "/123/test")))
+ .isEqualTo("/accounts/test");
+ }
+
+ @Test
+ public void getRestPathWithoutIds_nonEmptyContextPath() {
+ assertThat(getRestPathWithoutIds(fakeRequest("/c", "/a/accounts", "/123/test")))
+ .isEqualTo("/accounts/test");
+ assertThat(getRestPathWithoutIds(fakeRequest("/c", "/accounts", "/123/test")))
+ .isEqualTo("/accounts/test");
}
private FakeHttpServletRequest fakeRequest(