Merge "Add username to registration dialog"
diff --git a/.gitignore b/.gitignore
index 06a6e66..0e954ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@
/plugins/cookbook-plugin/
/test_site
/tools/format
+/.vscode
\ No newline at end of file
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index dada623..8774ed2 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2249,7 +2249,7 @@
link:rest-api-accounts.html#create-account[account creation] REST API and
inject additional external identifiers for an account that represents a user
in some external user store. For that, an implementation of the extension
-point `com.google.gerrit.server.api.accounts.AccountExternalIdCreator`
+point `com.google.gerrit.server.account.AccountExternalIdCreator`
must be registered.
[source,java]
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index 45c6e72..7c960dd 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -1,7 +1,5 @@
= Gerrit Code Review - PolyGerrit Plugin Styling
-== Plugin endpoints
-
Plugin should be html-based and imported following PolyGerrit's
link:pg-plugin-dev.html#loading[dev guide].
@@ -20,13 +18,52 @@
});
```
+== Default parameters
+All endpoints receive the following params, set as attributes to custom components
+that are instantiated at the endpoint:
+
+* `plugin`
++
+the current plugin instance, the one that is used by `Gerrit.install()`.
+
+* `content`
++
+decorated DOM Element, is only set for registrations that decorate existing
+components.
+
+== Plugin endpoints
+
+Following endpoints are available to plugins
+
=== change-view-integration
Extension point is located between `Files` and `Messages` section on the change
view page, and it may take full page's width. Primary purpose is to enable
plugins to display custom CI related information (build status, etc).
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
+
=== change-metadata-item
Extension point is located on the bottom of the change view left panel, under
`Label Status` and `Links` sections. It's width is equal to the left panel's and
primary purpose is to enable plugins to add sections of metadata to the left
panel.
+
+In addition to default parameters, the following are available:
+
+* `change`
++
+current change displayed, an instance of
+link:rest-api-changes.html#change-info[ChangeInfo]
+
+* `revision`
++
+current revision displayed, an instance of
+link:rest-api-changes.html#revision-info[RevisionInfo]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 5c1a29e..2cc78a4 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -324,6 +324,33 @@
}
----
+All::
+Get all projects, including those whose state is "HIDDEN".
++
+.Request
+----
+GET /projects/?all HTTP/1.0
+----
++
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "All-Projects" {
+ "id": "All-Projects",
+ "state": "ACTIVE"
+ },
+ "some-other-project": {
+ "id": "some-other-project",
+ "state": "HIDDEN"
+ }
+ }
+----
+
[[query-projects]]
=== Query Projects
--
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 636e909..b29922f 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -34,6 +34,7 @@
import com.google.gerrit.server.util.SocketUtil;
import com.google.gerrit.server.util.SystemLog;
import com.google.gerrit.testing.FakeEmailSender;
+import com.google.gerrit.testing.GroupNoteDbMode;
import com.google.gerrit.testing.NoteDbChecker;
import com.google.gerrit.testing.NoteDbMode;
import com.google.gerrit.testing.SshMode;
@@ -391,6 +392,7 @@
cfg.setBoolean("index", null, "reindexAfterRefUpdate", false);
NoteDbMode.newNotesMigrationFromEnv().setConfigValues(cfg);
+ GroupNoteDbMode.get().getGroupsMigration().setConfigValuesIfNotSetYet(cfg);
}
private static Injector createTestInjector(Daemon daemon) throws Exception {
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index 3f2cbda..4b1211b 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.config.TrackingFootersProvider;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeBundleReader;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.schema.DataSourceType;
@@ -78,6 +79,7 @@
bind(DataSourceType.class).to(InMemoryH2Type.class);
install(new NotesMigration.Module());
+ install(new GroupsMigration.Module());
TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
new TypeLiteral<SchemaFactory<ReviewDb>>() {};
bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
diff --git a/java/com/google/gerrit/extensions/api/projects/Projects.java b/java/com/google/gerrit/extensions/api/projects/Projects.java
index 02cce3a..b33f61e 100644
--- a/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -92,6 +92,7 @@
private int limit;
private int start;
private boolean showTree;
+ private boolean all;
private FilterType type = FilterType.ALL;
public List<ProjectInfo> get() throws RestApiException {
@@ -152,6 +153,11 @@
return this;
}
+ public ListRequest withAll() {
+ this.all = true;
+ return this;
+ }
+
public boolean getDescription() {
return description;
}
@@ -187,6 +193,10 @@
public FilterType getFilterType() {
return type;
}
+
+ public boolean isAll() {
+ return all;
+ }
}
/**
diff --git a/java/com/google/gerrit/extensions/common/testing/BUILD b/java/com/google/gerrit/extensions/common/testing/BUILD
index 160c14a..54d44c6 100644
--- a/java/com/google/gerrit/extensions/common/testing/BUILD
+++ b/java/com/google/gerrit/extensions/common/testing/BUILD
@@ -8,5 +8,6 @@
"//java/com/google/gerrit/extensions/client/testing:client-test-util",
"//java/com/google/gerrit/truth",
"//lib:truth",
+ "//lib/jgit/org.eclipse.jgit:jgit",
],
)
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index 7495a52..cdbef34 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -24,6 +24,8 @@
import com.google.common.truth.Truth;
import com.google.gerrit.extensions.common.GitPerson;
import java.sql.Timestamp;
+import java.util.Date;
+import org.eclipse.jgit.lib.PersonIdent;
public class GitPersonSubject extends Subject<GitPersonSubject, GitPerson> {
@@ -65,4 +67,14 @@
date().isEqualTo(other.date);
tz().isEqualTo(other.tz);
}
+
+ public void matches(PersonIdent ident) {
+ isNotNull();
+ name().isEqualTo(ident.getName());
+ email().isEqualTo(ident.getEmailAddress());
+ Truth.assertThat(new Date(actual().date.getTime()))
+ .named("rounded date")
+ .isEqualTo(ident.getWhen());
+ tz().isEqualTo(ident.getTimeZoneOffset());
+ }
}
diff --git a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
index 0df0aa9..967259a 100644
--- a/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
+++ b/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -27,7 +27,7 @@
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+import com.google.gerrit.server.account.GpgApiAdapter;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
diff --git a/java/com/google/gerrit/gpg/api/GpgApiModule.java b/java/com/google/gerrit/gpg/api/GpgApiModule.java
index f7102d8..f0d34f3 100644
--- a/java/com/google/gerrit/gpg/api/GpgApiModule.java
+++ b/java/com/google/gerrit/gpg/api/GpgApiModule.java
@@ -29,7 +29,7 @@
import com.google.gerrit.gpg.server.PostGpgKeys;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+import com.google.gerrit.server.account.GpgApiAdapter;
import java.util.List;
import java.util.Map;
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 8e1f898..7fc3074 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -17,6 +17,7 @@
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server:module",
+ "//java/com/google/gerrit/server/api",
"//java/com/google/gerrit/server/cache/h2",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/sshd",
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 44203dc..b5995a8 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -44,6 +44,7 @@
import com.google.gerrit.server.StartupChecks;
import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.InternalAccountDirectory;
+import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.change.ChangeCleanupRunner;
import com.google.gerrit.server.config.AuthConfig;
@@ -70,6 +71,7 @@
import com.google.gerrit.server.mail.receive.MailReceiver;
import com.google.gerrit.server.mail.send.SmtpEmailSender;
import com.google.gerrit.server.mime.MimeUtil2Module;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.patch.DiffExecutorModule;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
@@ -306,6 +308,7 @@
}
modules.add(new DatabaseModule());
modules.add(new NotesMigration.Module());
+ modules.add(new GroupsMigration.Module());
modules.add(new DropWizardMetricMaker.ApiModule());
return Guice.createInjector(PRODUCTION, modules);
}
@@ -330,6 +333,7 @@
modules.add(new DiffExecutorModule());
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+ modules.add(new GerritApiModule());
modules.add(new SearchingChangeCacheImpl.Module());
modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultPermissionBackendModule());
diff --git a/java/com/google/gerrit/index/QueryOptions.java b/java/com/google/gerrit/index/QueryOptions.java
index b57fb5f..0401dab 100644
--- a/java/com/google/gerrit/index/QueryOptions.java
+++ b/java/com/google/gerrit/index/QueryOptions.java
@@ -20,6 +20,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
import java.util.Set;
+import java.util.function.Function;
@AutoValue
public abstract class QueryOptions {
@@ -53,4 +54,8 @@
public QueryOptions withStart(int newStart) {
return create(config(), newStart, limit(), fields());
}
+
+ public QueryOptions filterFields(Function<QueryOptions, Set<String>> filter) {
+ return create(config(), start(), limit(), filter.apply(this));
+ }
}
diff --git a/java/com/google/gerrit/index/query/FieldBundle.java b/java/com/google/gerrit/index/query/FieldBundle.java
index 964dadf..6ecb6e6 100644
--- a/java/com/google/gerrit/index/query/FieldBundle.java
+++ b/java/com/google/gerrit/index/query/FieldBundle.java
@@ -55,8 +55,7 @@
Iterable<Object> result = fields.get(fieldDef.getName());
if (fieldDef.isRepeatable()) {
return (T) result;
- } else {
- return (T) Iterables.getOnlyElement(result);
}
+ return (T) Iterables.getOnlyElement(result);
}
}
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index f20bb18..0c261f6 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -31,13 +31,21 @@
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.FieldType;
import com.google.gerrit.index.Index;
+import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.Schema.Values;
+import com.google.gerrit.index.query.DataSource;
import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexUtils;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import java.io.IOException;
import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
@@ -48,6 +56,7 @@
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
@@ -62,9 +71,13 @@
import org.apache.lucene.index.TrackingIndexWriter;
import org.apache.lucene.search.ControlledRealTimeReopenThread;
import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
import org.apache.lucene.search.ReferenceManager;
import org.apache.lucene.search.ReferenceManager.RefreshListener;
+import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherFactory;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory;
import org.slf4j.Logger;
@@ -300,6 +313,8 @@
return result;
}
+ protected abstract V fromDocument(Document doc);
+
void add(Document doc, Values<V> values) {
String name = values.getField().getName();
FieldType<?> type = values.getField().getType();
@@ -445,4 +460,76 @@
public Schema<V> getSchema() {
return schema;
}
+
+ protected class LuceneQuerySource implements DataSource<V> {
+ private final QueryOptions opts;
+ private final Query query;
+ private final Sort sort;
+
+ LuceneQuerySource(QueryOptions opts, Query query, Sort sort) {
+ this.opts = opts;
+ this.query = query;
+ this.sort = sort;
+ }
+
+ @Override
+ public int getCardinality() {
+ return 10;
+ }
+
+ @Override
+ public ResultSet<V> read() throws OrmException {
+ return readImpl((doc) -> fromDocument(doc));
+ }
+
+ @Override
+ public ResultSet<FieldBundle> readRaw() throws OrmException {
+ return readImpl(AbstractLuceneIndex.this::toFieldBundle);
+ }
+
+ private <T> ResultSet<T> readImpl(Function<Document, T> mapper) throws OrmException {
+ IndexSearcher searcher = null;
+ try {
+ searcher = acquire();
+ int realLimit = opts.start() + opts.limit();
+ TopFieldDocs docs = searcher.search(query, realLimit, sort);
+ List<T> result = new ArrayList<>(docs.scoreDocs.length);
+ for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
+ ScoreDoc sd = docs.scoreDocs[i];
+ Document doc = searcher.doc(sd.doc, opts.fields());
+ T mapperResult = mapper.apply(doc);
+ if (mapperResult != null) {
+ result.add(mapperResult);
+ }
+ }
+ final List<T> r = Collections.unmodifiableList(result);
+ return new ResultSet<T>() {
+ @Override
+ public Iterator<T> iterator() {
+ return r.iterator();
+ }
+
+ @Override
+ public List<T> toList() {
+ return r;
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+ };
+ } catch (IOException e) {
+ throw new OrmException(e);
+ } finally {
+ if (searcher != null) {
+ try {
+ release(searcher);
+ } catch (IOException e) {
+ log.warn("cannot release Lucene searcher", e);
+ }
+ }
+ }
+ }
+ }
}
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index 26623ae..7d7cbef 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -87,6 +87,7 @@
}
// Make method public so that it can be used in LuceneChangeIndex
+ @Override
public FieldBundle toFieldBundle(Document doc) {
return super.toFieldBundle(doc);
}
@@ -104,4 +105,9 @@
}
super.add(doc, values);
}
+
+ @Override
+ protected ChangeData fromDocument(Document doc) {
+ throw new UnsupportedOperationException("don't use ChangeSubIndex directly");
+ }
}
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index 7a9c3cc..8ceea0d 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -19,7 +19,6 @@
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.DataSource;
-import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Account;
@@ -29,39 +28,24 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.index.account.AccountIndex;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherFactory;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
-import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.RAMDirectory;
import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class LuceneAccountIndex extends AbstractLuceneIndex<Account.Id, AccountState>
implements AccountIndex {
- private static final Logger log = LoggerFactory.getLogger(LuceneAccountIndex.class);
-
private static final String ACCOUNTS = "accounts";
private static final String ID_SORT_FIELD = sortFieldName(ID);
@@ -129,88 +113,14 @@
@Override
public DataSource<AccountState> getSource(Predicate<AccountState> p, QueryOptions opts)
throws QueryParseException {
- return new QuerySource(
- opts,
+ return new LuceneQuerySource(
+ opts.filterFields(IndexUtils::accountFields),
queryBuilder.toQuery(p),
new Sort(new SortField(ID_SORT_FIELD, SortField.Type.LONG, true)));
}
- private class QuerySource implements DataSource<AccountState> {
- private final QueryOptions opts;
- private final Query query;
- private final Sort sort;
-
- private QuerySource(QueryOptions opts, Query query, Sort sort) {
- this.opts = opts;
- this.query = query;
- this.sort = sort;
- }
-
- @Override
- public int getCardinality() {
- // TODO(dborowitz): In contrast to the comment in
- // LuceneChangeIndex.QuerySource#getCardinality, at this point I actually
- // think we might just want to remove getCardinality.
- return 10;
- }
-
- @Override
- public ResultSet<AccountState> read() throws OrmException {
- return readImpl(LuceneAccountIndex.this::toAccountState);
- }
-
- @Override
- public ResultSet<FieldBundle> readRaw() throws OrmException {
- return readImpl(LuceneAccountIndex.this::toFieldBundle);
- }
-
- private <T> ResultSet<T> readImpl(Function<Document, T> mapper) throws OrmException {
- IndexSearcher searcher = null;
- try {
- searcher = acquire();
- int realLimit = opts.start() + opts.limit();
- TopFieldDocs docs = searcher.search(query, realLimit, sort);
- List<T> result = new ArrayList<>(docs.scoreDocs.length);
- for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
- ScoreDoc sd = docs.scoreDocs[i];
- Document doc = searcher.doc(sd.doc, IndexUtils.accountFields(opts));
- T mapperResult = mapper.apply(doc);
- if (mapperResult != null) {
- result.add(mapperResult);
- }
- }
- final List<T> r = Collections.unmodifiableList(result);
- return new ResultSet<T>() {
- @Override
- public Iterator<T> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<T> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
- } finally {
- if (searcher != null) {
- try {
- release(searcher);
- } catch (IOException e) {
- log.warn("cannot release Lucene searcher", e);
- }
- }
- }
- }
- }
-
- private AccountState toAccountState(Document doc) {
+ @Override
+ protected AccountState fromDocument(Document doc) {
Account.Id id = new Account.Id(doc.getField(ID.getName()).numericValue().intValue());
// Use the AccountCache rather than depending on any stored fields in the
// document (of which there shouldn't be any). The most expensive part to
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index d479a17..7878afe 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -19,7 +19,6 @@
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.DataSource;
-import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -29,38 +28,24 @@
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherFactory;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
-import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.RAMDirectory;
import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class LuceneGroupIndex extends AbstractLuceneIndex<AccountGroup.UUID, InternalGroup>
implements GroupIndex {
- private static final Logger log = LoggerFactory.getLogger(LuceneGroupIndex.class);
private static final String GROUPS = "groups";
@@ -129,85 +114,14 @@
@Override
public DataSource<InternalGroup> getSource(Predicate<InternalGroup> p, QueryOptions opts)
throws QueryParseException {
- return new QuerySource(
- opts,
+ return new LuceneQuerySource(
+ opts.filterFields(IndexUtils::groupFields),
queryBuilder.toQuery(p),
new Sort(new SortField(UUID_SORT_FIELD, SortField.Type.STRING, false)));
}
- private class QuerySource implements DataSource<InternalGroup> {
- private final QueryOptions opts;
- private final Query query;
- private final Sort sort;
-
- private QuerySource(QueryOptions opts, Query query, Sort sort) {
- this.opts = opts;
- this.query = query;
- this.sort = sort;
- }
-
- @Override
- public int getCardinality() {
- return 10;
- }
-
- @Override
- public ResultSet<InternalGroup> read() throws OrmException {
- return readImpl(LuceneGroupIndex.this::toInternalGroup);
- }
-
- @Override
- public ResultSet<FieldBundle> readRaw() throws OrmException {
- return readImpl(LuceneGroupIndex.this::toFieldBundle);
- }
-
- private <T> ResultSet<T> readImpl(Function<Document, T> mapper) throws OrmException {
- IndexSearcher searcher = null;
- try {
- searcher = acquire();
- int realLimit = opts.start() + opts.limit();
- TopFieldDocs docs = searcher.search(query, realLimit, sort);
- List<T> result = new ArrayList<>(docs.scoreDocs.length);
- for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
- ScoreDoc sd = docs.scoreDocs[i];
- Document doc = searcher.doc(sd.doc, IndexUtils.groupFields(opts));
- T mapperResult = mapper.apply(doc);
- if (mapperResult != null) {
- result.add(mapperResult);
- }
- }
- final List<T> r = Collections.unmodifiableList(result);
- return new ResultSet<T>() {
- @Override
- public Iterator<T> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<T> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
- } finally {
- if (searcher != null) {
- try {
- release(searcher);
- } catch (IOException e) {
- log.warn("cannot release Lucene searcher", e);
- }
- }
- }
- }
- }
-
- private InternalGroup toInternalGroup(Document doc) {
+ @Override
+ protected InternalGroup fromDocument(Document doc) {
AccountGroup.UUID uuid = new AccountGroup.UUID(doc.getField(UUID.getName()).stringValue());
// Use the GroupCache rather than depending on any stored fields in the
// document (of which there shouldn't be any).
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 3e6f50a..e776a8b 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -19,7 +19,6 @@
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.DataSource;
-import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.Project;
@@ -29,39 +28,24 @@
import com.google.gerrit.server.index.project.ProjectIndex;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectData;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherFactory;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
-import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.RAMDirectory;
import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class LuceneProjectIndex extends AbstractLuceneIndex<Project.NameKey, ProjectData>
implements ProjectIndex {
- private static final Logger log = LoggerFactory.getLogger(LuceneProjectIndex.class);
-
private static final String PROJECTS = "projects";
private static final String NAME_SORT_FIELD = sortFieldName(NAME);
@@ -129,85 +113,14 @@
@Override
public DataSource<ProjectData> getSource(Predicate<ProjectData> p, QueryOptions opts)
throws QueryParseException {
- return new QuerySource(
- opts,
+ return new LuceneQuerySource(
+ opts.filterFields(IndexUtils::projectFields),
queryBuilder.toQuery(p),
new Sort(new SortField(NAME_SORT_FIELD, SortField.Type.STRING, false)));
}
- private class QuerySource implements DataSource<ProjectData> {
- private final QueryOptions opts;
- private final Query query;
- private final Sort sort;
-
- private QuerySource(QueryOptions opts, Query query, Sort sort) {
- this.opts = opts;
- this.query = query;
- this.sort = sort;
- }
-
- @Override
- public int getCardinality() {
- return 10;
- }
-
- @Override
- public ResultSet<ProjectData> read() throws OrmException {
- return readImpl(LuceneProjectIndex.this::toProjectData);
- }
-
- @Override
- public ResultSet<FieldBundle> readRaw() throws OrmException {
- return readImpl(LuceneProjectIndex.this::toFieldBundle);
- }
-
- private <T> ResultSet<T> readImpl(Function<Document, T> mapper) throws OrmException {
- IndexSearcher searcher = null;
- try {
- searcher = acquire();
- int realLimit = opts.start() + opts.limit();
- TopFieldDocs docs = searcher.search(query, realLimit, sort);
- List<T> result = new ArrayList<>(docs.scoreDocs.length);
- for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
- ScoreDoc sd = docs.scoreDocs[i];
- Document doc = searcher.doc(sd.doc, IndexUtils.projectFields(opts));
- T mapperResult = mapper.apply(doc);
- if (mapperResult != null) {
- result.add(mapperResult);
- }
- }
- final List<T> r = Collections.unmodifiableList(result);
- return new ResultSet<T>() {
- @Override
- public Iterator<T> iterator() {
- return r.iterator();
- }
-
- @Override
- public List<T> toList() {
- return r;
- }
-
- @Override
- public void close() {
- // Do nothing.
- }
- };
- } catch (IOException e) {
- throw new OrmException(e);
- } finally {
- if (searcher != null) {
- try {
- release(searcher);
- } catch (IOException e) {
- log.warn("cannot release Lucene searcher", e);
- }
- }
- }
- }
- }
-
- private ProjectData toProjectData(Document doc) {
+ @Override
+ protected ProjectData fromDocument(Document doc) {
Project.NameKey nameKey = new Project.NameKey(doc.getField(NAME.getName()).stringValue());
return projectCache.get().get(nameKey).toProjectData();
}
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index dce3ea1..4dfaf1c 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -35,6 +35,7 @@
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server:module",
+ "//java/com/google/gerrit/server/api",
"//java/com/google/gerrit/server/cache/h2",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/sshd",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index a369953..d72b46f 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -52,6 +52,7 @@
import com.google.gerrit.server.StartupChecks;
import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.InternalAccountDirectory;
+import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.change.ChangeCleanupRunner;
import com.google.gerrit.server.config.AuthConfig;
@@ -411,6 +412,8 @@
modules.add(new DiffExecutorModule());
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+ modules.add(new GerritApiModule());
+
modules.add(new SearchingChangeCacheImpl.Module(slave));
modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultPermissionBackendModule());
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index e6f05cf..3385244 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -42,6 +42,7 @@
import com.google.gerrit.server.group.db.GroupNameNotes;
import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -72,20 +73,14 @@
private final InitFlags flags;
private final SitePaths site;
private final String allUsers;
- private final boolean readFromNoteDb;
- private final boolean writeGroupsToNoteDb;
+ private final GroupsMigration groupsMigration;
@Inject
public GroupsOnInit(InitFlags flags, SitePaths site, AllUsersNameOnInitProvider allUsers) {
this.flags = flags;
this.site = site;
this.allUsers = allUsers.get();
- readFromNoteDb = flags.cfg.getBoolean("user", null, "readGroupsFromNoteDb", false);
- // TODO(aliceks): Remove this flag when all other necessary TODOs for writing groups to NoteDb
- // have been addressed.
- // Don't flip this flag in a production setting! We only added it to spread the implementation
- // of groups in NoteDb among several changes which are gradually merged.
- writeGroupsToNoteDb = flags.cfg.getBoolean("user", null, "writeGroupsToNoteDb", false);
+ this.groupsMigration = new GroupsMigration(flags.cfg);
}
/**
@@ -101,7 +96,7 @@
*/
public InternalGroup getExistingGroup(ReviewDb db, GroupReference groupReference)
throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
- if (readFromNoteDb) {
+ if (groupsMigration.readFromNoteDb()) {
return getExistingGroupFromNoteDb(groupReference);
}
@@ -151,7 +146,7 @@
*/
public Stream<GroupReference> getAllGroupReferences(ReviewDb db)
throws OrmException, IOException, ConfigInvalidException {
- if (readFromNoteDb) {
+ if (groupsMigration.readFromNoteDb()) {
File allUsersRepoPath = getPathToAllUsersRepository();
if (allUsersRepoPath != null) {
try (Repository allUsersRepo = new FileRepository(allUsersRepoPath)) {
@@ -181,7 +176,7 @@
public void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, Account account)
throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
addGroupMemberInReviewDb(db, groupUuid, account.getId());
- if (!writeGroupsToNoteDb) {
+ if (!groupsMigration.writeToNoteDb()) {
return;
}
addGroupMemberInNoteDb(groupUuid, account);
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index b59e085..afabcf6 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.config.GerritServerConfigModule;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.git.GitRepositoryManagerModule;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.schema.DataSourceModule;
import com.google.gerrit.server.schema.DataSourceProvider;
@@ -183,6 +184,7 @@
modules.add(new SchemaModule());
modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
modules.add(new NotesMigration.Module());
+ modules.add(new GroupsMigration.Module());
try {
return Guice.createInjector(PRODUCTION, modules);
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index 2dd5b36..c7dc420 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -286,4 +286,25 @@
return Objects.hash(
name, groupId, description, visibleToAll, groupUUID, ownerGroupUUID, createdOn);
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "{"
+ + "name="
+ + name
+ + ", groupId="
+ + groupId
+ + ", description="
+ + description
+ + ", visibleToAll="
+ + visibleToAll
+ + ", groupUUID="
+ + groupUUID
+ + ", ownerGroupUUID="
+ + ownerGroupUUID
+ + ", createdOn="
+ + createdOn
+ + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupById.java b/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
index 30ca38f..17a205e 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
@@ -88,4 +88,9 @@
public int hashCode() {
return key.hashCode();
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{key=" + key + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java b/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
index 33955c4..5246d72 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
@@ -142,4 +142,19 @@
public int hashCode() {
return Objects.hash(key, addedBy, removedBy, removedOn);
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "{"
+ + "key="
+ + key
+ + ", addedBy="
+ + addedBy
+ + ", removedBy="
+ + removedBy
+ + ", removedOn="
+ + removedOn
+ + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java b/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
index ea46366..e1e0754 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
@@ -84,4 +84,9 @@
public int hashCode() {
return key.hashCode();
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{key=" + key + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
index 9968b7d..4ea19d2 100644
--- a/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ b/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -147,4 +147,19 @@
public int hashCode() {
return Objects.hash(key, addedBy, removedBy, removedOn);
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "{"
+ + "key="
+ + key
+ + ", addedBy="
+ + addedBy
+ + ", removedBy="
+ + removedBy
+ + ", removedOn="
+ + removedOn
+ + "}";
+ }
}
diff --git a/java/com/google/gerrit/reviewdb/client/RefNames.java b/java/com/google/gerrit/reviewdb/client/RefNames.java
index 1839d11..995b020 100644
--- a/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -224,6 +224,10 @@
return ref.startsWith(REFS_USERS);
}
+ public static boolean isRefsGroups(String ref) {
+ return ref.startsWith(REFS_GROUPS);
+ }
+
static Integer parseShardedRefPart(String name) {
if (name == null) {
return null;
diff --git a/java/com/google/gerrit/reviewdb/server/DisallowReadFromGroupsReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/DisallowReadFromGroupsReviewDbWrapper.java
new file mode 100644
index 0000000..640924c
--- /dev/null
+++ b/java/com/google/gerrit/reviewdb/server/DisallowReadFromGroupsReviewDbWrapper.java
@@ -0,0 +1,310 @@
+// Copyright (C) 2017 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.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+public class DisallowReadFromGroupsReviewDbWrapper extends ReviewDbWrapper {
+ private static final String MSG = "This table has been migrated to NoteDb";
+
+ private final Groups groups;
+ private final GroupNames groupNames;
+ private final GroupMembers groupMembers;
+ private final GroupMemberAudits groupMemberAudits;
+ private final ByIds byIds;
+ private final ByIdAudits byIdAudits;
+
+ public DisallowReadFromGroupsReviewDbWrapper(ReviewDb db) {
+ super(db);
+ groups = new Groups(delegate.accountGroups());
+ groupNames = new GroupNames(delegate.accountGroupNames());
+ groupMembers = new GroupMembers(delegate.accountGroupMembers());
+ groupMemberAudits = new GroupMemberAudits(delegate.accountGroupMembersAudit());
+ byIds = new ByIds(delegate.accountGroupById());
+ byIdAudits = new ByIdAudits(delegate.accountGroupByIdAud());
+ }
+
+ public ReviewDb unsafeGetDelegate() {
+ return delegate;
+ }
+
+ @Override
+ public AccountGroupAccess accountGroups() {
+ return groups;
+ }
+
+ @Override
+ public AccountGroupNameAccess accountGroupNames() {
+ return groupNames;
+ }
+
+ @Override
+ public AccountGroupMemberAccess accountGroupMembers() {
+ return groupMembers;
+ }
+
+ @Override
+ public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
+ return groupMemberAudits;
+ }
+
+ @Override
+ public AccountGroupByIdAccess accountGroupById() {
+ return byIds;
+ }
+
+ @Override
+ public AccountGroupByIdAudAccess accountGroupByIdAud() {
+ return byIdAudits;
+ }
+
+ private static class Groups extends AccountGroupAccessWrapper {
+ protected Groups(AccountGroupAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroup, OrmException> getAsync(
+ AccountGroup.Id key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> get(Iterable<AccountGroup.Id> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroup get(AccountGroup.Id id) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> all() {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class GroupNames extends AccountGroupNameAccessWrapper {
+ protected GroupNames(AccountGroupNameAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupName, OrmException> getAsync(
+ AccountGroup.NameKey key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> get(Iterable<AccountGroup.NameKey> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupName get(AccountGroup.NameKey name) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> all() {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class GroupMembers extends AccountGroupMemberAccessWrapper {
+ protected GroupMembers(AccountGroupMemberAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupMember, OrmException>
+ getAsync(AccountGroupMember.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> get(Iterable<AccountGroupMember.Key> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupMember get(AccountGroupMember.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byAccount(Account.Id id) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class GroupMemberAudits extends AccountGroupMemberAuditAccessWrapper {
+ protected GroupMemberAudits(AccountGroupMemberAuditAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupMemberAudit, OrmException>
+ getAsync(AccountGroupMemberAudit.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> get(Iterable<AccountGroupMemberAudit.Key> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupMemberAudit get(AccountGroupMemberAudit.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroupAccount(
+ AccountGroup.Id groupId, Account.Id accountId) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId) {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class ByIds extends AccountGroupByIdAccessWrapper {
+ protected ByIds(AccountGroupByIdAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupById, OrmException> getAsync(
+ AccountGroupById.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> get(Iterable<AccountGroupById.Key> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupById get(AccountGroupById.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> all() {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+
+ private static class ByIdAudits extends AccountGroupByIdAudAccessWrapper {
+ protected ByIdAudits(AccountGroupByIdAudAccess delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> iterateAllEntities() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupByIdAud, OrmException>
+ getAsync(AccountGroupByIdAud.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> get(Iterable<AccountGroupByIdAud.Key> keys) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public AccountGroupByIdAud get(AccountGroupByIdAud.Key key) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroupInclude(
+ AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId) {
+ throw new UnsupportedOperationException(MSG);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java b/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
index bb31b1c..ef057eb 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
@@ -14,8 +14,16 @@
package com.google.gerrit.reviewdb.server;
+import static com.google.common.base.Preconditions.checkState;
+
import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.IntKey;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.TreeSet;
/** Static utilities for ReviewDb types. */
public class ReviewDbUtil {
@@ -43,10 +51,30 @@
public static ReviewDb unwrapDb(ReviewDb db) {
if (db instanceof DisallowReadFromChangesReviewDbWrapper) {
- return ((DisallowReadFromChangesReviewDbWrapper) db).unsafeGetDelegate();
+ return unwrapDb(((DisallowReadFromChangesReviewDbWrapper) db).unsafeGetDelegate());
+ }
+ if (db instanceof DisallowReadFromGroupsReviewDbWrapper) {
+ return unwrapDb(((DisallowReadFromGroupsReviewDbWrapper) db).unsafeGetDelegate());
}
return db;
}
+ public static void checkColumns(Class<?> clazz, Integer... expected) {
+ Set<Integer> ids = new TreeSet<>();
+ for (Field f : clazz.getDeclaredFields()) {
+ Column col = f.getAnnotation(Column.class);
+ if (col != null) {
+ ids.add(col.id());
+ }
+ }
+ Set<Integer> expectedIds = Sets.newTreeSet(Arrays.asList(expected));
+ checkState(
+ ids.equals(expectedIds),
+ "Unexpected column set for %s: %s != %s",
+ clazz.getSimpleName(),
+ ids,
+ expectedIds);
+ }
+
private ReviewDbUtil() {}
}
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 7fd2c73..efc8a01 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -17,6 +17,12 @@
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchLineComment;
@@ -679,4 +685,591 @@
return delegate.all();
}
}
+
+ public static class AccountGroupAccessWrapper implements AccountGroupAccess {
+ protected final AccountGroupAccess delegate;
+
+ protected AccountGroupAccessWrapper(AccountGroupAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroup> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroup.Id primaryKey(AccountGroup entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroup.Id, AccountGroup> toMap(Iterable<AccountGroup> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroup, OrmException> getAsync(
+ AccountGroup.Id key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> get(Iterable<AccountGroup.Id> keys) throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroup> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroup> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroup> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroup.Id> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroup> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroup.Id key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroup atomicUpdate(AccountGroup.Id key, AtomicUpdate<AccountGroup> update)
+ throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroup get(AccountGroup.Id id) throws OrmException {
+ return delegate.get(id);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException {
+ return delegate.byUUID(uuid);
+ }
+
+ @Override
+ public ResultSet<AccountGroup> all() throws OrmException {
+ return delegate.all();
+ }
+ }
+
+ public static class AccountGroupNameAccessWrapper implements AccountGroupNameAccess {
+ protected final AccountGroupNameAccess delegate;
+
+ protected AccountGroupNameAccessWrapper(AccountGroupNameAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroup.NameKey primaryKey(AccountGroupName entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroup.NameKey, AccountGroupName> toMap(Iterable<AccountGroupName> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupName, OrmException> getAsync(
+ AccountGroup.NameKey key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> get(Iterable<AccountGroup.NameKey> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupName> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupName> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupName> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroup.NameKey> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupName> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroup.NameKey key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupName atomicUpdate(
+ AccountGroup.NameKey key, AtomicUpdate<AccountGroupName> update) throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupName get(AccountGroup.NameKey name) throws OrmException {
+ return delegate.get(name);
+ }
+
+ @Override
+ public ResultSet<AccountGroupName> all() throws OrmException {
+ return delegate.all();
+ }
+ }
+
+ public static class AccountGroupMemberAccessWrapper implements AccountGroupMemberAccess {
+ protected final AccountGroupMemberAccess delegate;
+
+ protected AccountGroupMemberAccessWrapper(AccountGroupMemberAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroupMember.Key primaryKey(AccountGroupMember entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroupMember.Key, AccountGroupMember> toMap(Iterable<AccountGroupMember> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupMember, OrmException>
+ getAsync(AccountGroupMember.Key key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> get(Iterable<AccountGroupMember.Key> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupMember> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupMember> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupMember> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroupMember.Key> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupMember> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroupMember.Key key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupMember atomicUpdate(
+ AccountGroupMember.Key key, AtomicUpdate<AccountGroupMember> update) throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupMember get(AccountGroupMember.Key key) throws OrmException {
+ return delegate.get(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byAccount(Account.Id id) throws OrmException {
+ return delegate.byAccount(id);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) throws OrmException {
+ return delegate.byGroup(id);
+ }
+ }
+
+ public static class AccountGroupMemberAuditAccessWrapper
+ implements AccountGroupMemberAuditAccess {
+ protected final AccountGroupMemberAuditAccess delegate;
+
+ protected AccountGroupMemberAuditAccessWrapper(AccountGroupMemberAuditAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroupMemberAudit.Key primaryKey(AccountGroupMemberAudit entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroupMemberAudit.Key, AccountGroupMemberAudit> toMap(
+ Iterable<AccountGroupMemberAudit> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupMemberAudit, OrmException>
+ getAsync(AccountGroupMemberAudit.Key key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> get(Iterable<AccountGroupMemberAudit.Key> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroupMemberAudit.Key> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupMemberAudit> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroupMemberAudit.Key key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupMemberAudit atomicUpdate(
+ AccountGroupMemberAudit.Key key, AtomicUpdate<AccountGroupMemberAudit> update)
+ throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupMemberAudit get(AccountGroupMemberAudit.Key key) throws OrmException {
+ return delegate.get(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroupAccount(
+ AccountGroup.Id groupId, Account.Id accountId) throws OrmException {
+ return delegate.byGroupAccount(groupId, accountId);
+ }
+
+ @Override
+ public ResultSet<AccountGroupMemberAudit> byGroup(AccountGroup.Id groupId) throws OrmException {
+ return delegate.byGroup(groupId);
+ }
+ }
+
+ public static class AccountGroupByIdAccessWrapper implements AccountGroupByIdAccess {
+ protected final AccountGroupByIdAccess delegate;
+
+ protected AccountGroupByIdAccessWrapper(AccountGroupByIdAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroupById.Key primaryKey(AccountGroupById entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroupById.Key, AccountGroupById> toMap(Iterable<AccountGroupById> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupById, OrmException> getAsync(
+ AccountGroupById.Key key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> get(Iterable<AccountGroupById.Key> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupById> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupById> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupById> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroupById.Key> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupById> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroupById.Key key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupById atomicUpdate(
+ AccountGroupById.Key key, AtomicUpdate<AccountGroupById> update) throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupById get(AccountGroupById.Key key) throws OrmException {
+ return delegate.get(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException {
+ return delegate.byIncludeUUID(uuid);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) throws OrmException {
+ return delegate.byGroup(id);
+ }
+
+ @Override
+ public ResultSet<AccountGroupById> all() throws OrmException {
+ return delegate.all();
+ }
+ }
+
+ public static class AccountGroupByIdAudAccessWrapper implements AccountGroupByIdAudAccess {
+ protected final AccountGroupByIdAudAccess delegate;
+
+ protected AccountGroupByIdAudAccessWrapper(AccountGroupByIdAudAccess delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public String getRelationName() {
+ return delegate.getRelationName();
+ }
+
+ @Override
+ public int getRelationID() {
+ return delegate.getRelationID();
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> iterateAllEntities() throws OrmException {
+ return delegate.iterateAllEntities();
+ }
+
+ @Override
+ public AccountGroupByIdAud.Key primaryKey(AccountGroupByIdAud entity) {
+ return delegate.primaryKey(entity);
+ }
+
+ @Override
+ public Map<AccountGroupByIdAud.Key, AccountGroupByIdAud> toMap(
+ Iterable<AccountGroupByIdAud> c) {
+ return delegate.toMap(c);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public com.google.common.util.concurrent.CheckedFuture<AccountGroupByIdAud, OrmException>
+ getAsync(AccountGroupByIdAud.Key key) {
+ return delegate.getAsync(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> get(Iterable<AccountGroupByIdAud.Key> keys)
+ throws OrmException {
+ return delegate.get(keys);
+ }
+
+ @Override
+ public void insert(Iterable<AccountGroupByIdAud> instances) throws OrmException {
+ delegate.insert(instances);
+ }
+
+ @Override
+ public void update(Iterable<AccountGroupByIdAud> instances) throws OrmException {
+ delegate.update(instances);
+ }
+
+ @Override
+ public void upsert(Iterable<AccountGroupByIdAud> instances) throws OrmException {
+ delegate.upsert(instances);
+ }
+
+ @Override
+ public void deleteKeys(Iterable<AccountGroupByIdAud.Key> keys) throws OrmException {
+ delegate.deleteKeys(keys);
+ }
+
+ @Override
+ public void delete(Iterable<AccountGroupByIdAud> instances) throws OrmException {
+ delegate.delete(instances);
+ }
+
+ @Override
+ public void beginTransaction(AccountGroupByIdAud.Key key) throws OrmException {
+ delegate.beginTransaction(key);
+ }
+
+ @Override
+ public AccountGroupByIdAud atomicUpdate(
+ AccountGroupByIdAud.Key key, AtomicUpdate<AccountGroupByIdAud> update) throws OrmException {
+ return delegate.atomicUpdate(key, update);
+ }
+
+ @Override
+ public AccountGroupByIdAud get(AccountGroupByIdAud.Key key) throws OrmException {
+ return delegate.get(key);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroupInclude(
+ AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID) throws OrmException {
+ return delegate.byGroupInclude(groupId, incGroupUUID);
+ }
+
+ @Override
+ public ResultSet<AccountGroupByIdAud> byGroup(AccountGroup.Id groupId) throws OrmException {
+ return delegate.byGroup(groupId);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/account/AccountDeactivator.java b/java/com/google/gerrit/server/account/AccountDeactivator.java
index c222756..c56caac 100644
--- a/java/com/google/gerrit/server/account/AccountDeactivator.java
+++ b/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.ScheduleConfig;
@@ -96,24 +97,42 @@
@Override
public void run() {
- log.debug("Running account deactivations");
+ log.info("Running account deactivations");
try {
int numberOfAccountsDeactivated = 0;
for (AccountState acc : accountQueryProvider.get().query(AccountPredicates.isActive())) {
- log.debug("processing account " + acc.getUserName());
- if (acc.getUserName() != null && !realm.isActive(acc.getUserName())) {
- sif.deactivate(acc.getAccount().getId());
- log.debug("deactivated accout " + acc.getUserName());
+ if (processAccount(acc)) {
numberOfAccountsDeactivated++;
}
}
log.info(
"Deactivations complete, {} account(s) were deactivated", numberOfAccountsDeactivated);
} catch (Exception e) {
- log.error("Failed to deactivate inactive accounts " + e.getMessage(), e);
+ log.error("Failed to complete deactivation of accounts: " + e.getMessage(), e);
}
}
+ private boolean processAccount(AccountState account) {
+ log.debug("processing account " + account.getUserName());
+ try {
+ if (account.getUserName() != null && !realm.isActive(account.getUserName())) {
+ sif.deactivate(account.getAccount().getId());
+ log.info("deactivated account " + account.getUserName());
+ return true;
+ }
+ } catch (ResourceConflictException e) {
+ log.info("Account {} already deactivated, continuing...", account.getUserName());
+ } catch (Exception e) {
+ log.error(
+ "Error deactivating account: {} ({}) {}",
+ account.getUserName(),
+ account.getAccount().getId(),
+ e.getMessage(),
+ e);
+ }
+ return false;
+ }
+
@Override
public String toString() {
return "account deactivator";
diff --git a/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java b/java/com/google/gerrit/server/account/AccountExternalIdCreator.java
similarity index 96%
rename from java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
rename to java/com/google/gerrit/server/account/AccountExternalIdCreator.java
index 2f8dee6..8cf4ee0 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
+++ b/java/com/google/gerrit/server/account/AccountExternalIdCreator.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.api.accounts;
+package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.externalids.ExternalId;
diff --git a/java/com/google/gerrit/server/api/accounts/AccountInfoComparator.java b/java/com/google/gerrit/server/account/AccountInfoComparator.java
similarity index 97%
rename from java/com/google/gerrit/server/api/accounts/AccountInfoComparator.java
rename to java/com/google/gerrit/server/account/AccountInfoComparator.java
index 7c468fc..533dece 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountInfoComparator.java
+++ b/java/com/google/gerrit/server/account/AccountInfoComparator.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.api.accounts;
+package com.google.gerrit.server.account;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Ordering;
diff --git a/java/com/google/gerrit/server/account/Accounts.java b/java/com/google/gerrit/server/account/Accounts.java
index a9428f0..8d39fb1 100644
--- a/java/com/google/gerrit/server/account/Accounts.java
+++ b/java/com/google/gerrit/server/account/Accounts.java
@@ -18,6 +18,7 @@
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.config.AllUsersName;
@@ -56,6 +57,7 @@
this.emailValidator = emailValidator;
}
+ @Nullable
public Account get(Account.Id accountId) throws IOException, ConfigInvalidException {
try (Repository repo = repoManager.openRepository(allUsersName)) {
return read(repo, accountId);
@@ -133,6 +135,7 @@
}
}
+ @Nullable
private Account read(Repository allUsersRepository, Account.Id accountId)
throws IOException, ConfigInvalidException {
AccountConfig accountConfig = new AccountConfig(emailValidator, accountId);
diff --git a/java/com/google/gerrit/server/account/CreateAccount.java b/java/com/google/gerrit/server/account/CreateAccount.java
index a5d2e7a..ed92a7e 100644
--- a/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/account/CreateAccount.java
@@ -40,7 +40,6 @@
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
-import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
import com.google.gerrit.server.group.GroupsCollection;
import com.google.gerrit.server.group.UserInitiated;
import com.google.gerrit.server.group.db.GroupsUpdate;
diff --git a/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java b/java/com/google/gerrit/server/account/GpgApiAdapter.java
similarity index 93%
rename from java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
rename to java/com/google/gerrit/server/account/GpgApiAdapter.java
index 7def6fa..b060140 100644
--- a/java/com/google/gerrit/server/api/accounts/GpgApiAdapter.java
+++ b/java/com/google/gerrit/server/account/GpgApiAdapter.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.api.accounts;
+package com.google.gerrit.server.account;
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
import com.google.gerrit.extensions.common.GpgKeyInfo;
@@ -21,7 +21,6 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountResource;
import java.util.List;
import java.util.Map;
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index 16655ab..119dc5b 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -144,6 +144,7 @@
return isOwner;
}
+ // Keep this logic in sync with VisibleRefFilter#isOwner(...).
if (group instanceof GroupDescription.Internal) {
AccountGroup.UUID ownerUUID = ((GroupDescription.Internal) group).getOwnerGroupUUID();
isOwner = getUser().getEffectiveGroups().contains(ownerUUID) || canAdministrateServer();
diff --git a/java/com/google/gerrit/server/account/QueryAccounts.java b/java/com/google/gerrit/server/account/QueryAccounts.java
index e6ac0f6..88f0bbc 100644
--- a/java/com/google/gerrit/server/account/QueryAccounts.java
+++ b/java/com/google/gerrit/server/account/QueryAccounts.java
@@ -28,7 +28,6 @@
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
-import com.google.gerrit.server.api.accounts.AccountInfoComparator;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.query.account.AccountPredicates;
import com.google.gerrit.server.query.account.AccountQueryBuilder;
diff --git a/java/com/google/gerrit/server/api/BUILD b/java/com/google/gerrit/server/api/BUILD
new file mode 100644
index 0000000..2741a0a
--- /dev/null
+++ b/java/com/google/gerrit/server/api/BUILD
@@ -0,0 +1,21 @@
+java_library(
+ name = "api",
+ srcs = glob(
+ ["**/*.java"],
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/common:annotations",
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/lifecycle",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//lib:guava",
+ "//lib:gwtorm",
+ "//lib:servlet-api-3_1",
+ "//lib/guice",
+ "//lib/guice:guice-assistedinject",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/java/com/google/gerrit/server/api/Module.java b/java/com/google/gerrit/server/api/GerritApiModule.java
similarity index 75%
rename from java/com/google/gerrit/server/api/Module.java
rename to java/com/google/gerrit/server/api/GerritApiModule.java
index 6214129..81226b0 100644
--- a/java/com/google/gerrit/server/api/Module.java
+++ b/java/com/google/gerrit/server/api/GerritApiModule.java
@@ -15,9 +15,12 @@
package com.google.gerrit.server.api;
import com.google.gerrit.extensions.api.GerritApi;
-import com.google.inject.AbstractModule;
+import com.google.gerrit.extensions.api.plugins.Plugins;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.api.plugins.PluginApiImpl;
+import com.google.gerrit.server.api.plugins.PluginsImpl;
-public class Module extends AbstractModule {
+public class GerritApiModule extends FactoryModule {
@Override
protected void configure() {
bind(GerritApi.class).to(GerritApiImpl.class);
@@ -27,5 +30,8 @@
install(new com.google.gerrit.server.api.config.Module());
install(new com.google.gerrit.server.api.groups.Module());
install(new com.google.gerrit.server.api.projects.Module());
+
+ bind(Plugins.class).to(PluginsImpl.class);
+ factory(PluginApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index f36322c..7b14725 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -62,6 +62,7 @@
import com.google.gerrit.server.account.GetPreferences;
import com.google.gerrit.server.account.GetSshKeys;
import com.google.gerrit.server.account.GetWatchedProjects;
+import com.google.gerrit.server.account.GpgApiAdapter;
import com.google.gerrit.server.account.Index;
import com.google.gerrit.server.account.PostWatchedProjects;
import com.google.gerrit.server.account.PutActive;
diff --git a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index 9490075..0438a23 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -129,6 +129,8 @@
}
lp.setFilterType(type);
+ lp.setAll(request.isAll());
+
return lp.apply();
}
diff --git a/java/com/google/gerrit/server/change/Abandon.java b/java/com/google/gerrit/server/change/Abandon.java
index c9d016d..c7addff 100644
--- a/java/com/google/gerrit/server/change/Abandon.java
+++ b/java/com/google/gerrit/server/change/Abandon.java
@@ -31,7 +31,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.git.AbandonOp;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
diff --git a/java/com/google/gerrit/server/git/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
similarity index 98%
rename from java/com/google/gerrit/server/git/AbandonOp.java
rename to java/com/google/gerrit/server/change/AbandonOp.java
index 8298db3..cbe5e2b 100644
--- a/java/com/google/gerrit/server/git/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.change;
import com.google.common.base.Strings;
import com.google.common.collect.ListMultimap;
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 7563d99..a03f60a 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -105,9 +105,9 @@
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.WebLinks;
+import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.api.accounts.AccountInfoComparator;
-import com.google.gerrit.server.api.accounts.GpgApiAdapter;
+import com.google.gerrit.server.account.GpgApiAdapter;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 8f5989a..c272f6c 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -79,6 +79,7 @@
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountDeactivator;
+import com.google.gerrit.server.account.AccountExternalIdCreator;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountVisibilityProvider;
@@ -90,13 +91,13 @@
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.ExternalIdModule;
-import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.UniversalAuthBackend;
import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
import com.google.gerrit.server.avatar.AvatarProvider;
import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.gerrit.server.change.AbandonOp;
import com.google.gerrit.server.change.AccountPatchReviewStore;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeKindCacheImpl;
@@ -108,7 +109,6 @@
import com.google.gerrit.server.events.UserScopedEventListener;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.extensions.webui.UiActions;
-import com.google.gerrit.server.git.AbandonOp;
import com.google.gerrit.server.git.ChangeMessageModifier;
import com.google.gerrit.server.git.EmailMerge;
import com.google.gerrit.server.git.GitModule;
@@ -300,7 +300,6 @@
bind(UiActions.class);
install(new com.google.gerrit.server.access.Module());
install(new com.google.gerrit.server.account.Module());
- install(new com.google.gerrit.server.api.Module());
install(new com.google.gerrit.server.change.Module());
install(new com.google.gerrit.server.config.Module());
install(new com.google.gerrit.server.group.Module());
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index fd512a5..b0f10f2 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -14,14 +14,22 @@
package com.google.gerrit.server.git;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.server.CommonConverters;
+import java.io.IOException;
import java.util.ArrayList;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
/** Static utilities for working with {@link RevCommit}s. */
public class CommitUtil {
- public static CommitInfo toCommitInfo(RevCommit commit) {
+ public static CommitInfo toCommitInfo(RevCommit commit) throws IOException {
+ return toCommitInfo(commit, null);
+ }
+
+ public static CommitInfo toCommitInfo(RevCommit commit, @Nullable RevWalk walk)
+ throws IOException {
CommitInfo info = new CommitInfo();
info.commit = commit.getName();
info.author = CommonConverters.toGitPerson(commit.getAuthorIdent());
@@ -30,7 +38,7 @@
info.message = commit.getFullMessage();
info.parents = new ArrayList<>(commit.getParentCount());
for (int i = 0; i < commit.getParentCount(); i++) {
- RevCommit p = commit.getParent(i);
+ RevCommit p = walk == null ? commit.getParent(i) : walk.parseCommit(commit.getParent(i));
CommitInfo parentInfo = new CommitInfo();
parentInfo.commit = p.getName();
parentInfo.subject = p.getShortMessage();
diff --git a/java/com/google/gerrit/server/git/VersionedMetaData.java b/java/com/google/gerrit/server/git/VersionedMetaData.java
index 74a8134..812e693 100644
--- a/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.MoreObjects;
+import com.google.gerrit.common.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
@@ -76,7 +77,9 @@
}
}
- protected RevCommit revision;
+ /** The revision at which the data was loaded. Is null for data yet to be created. */
+ @Nullable protected RevCommit revision;
+
protected RevWalk rw;
protected ObjectReader reader;
protected ObjectInserter inserter;
@@ -133,7 +136,8 @@
* @throws IOException
* @throws ConfigInvalidException
*/
- public void load(Repository db, ObjectId id) throws IOException, ConfigInvalidException {
+ public void load(Repository db, @Nullable ObjectId id)
+ throws IOException, ConfigInvalidException {
try (RevWalk walk = new RevWalk(db)) {
load(walk, id);
}
diff --git a/java/com/google/gerrit/server/git/VisibleRefFilter.java b/java/com/google/gerrit/server/git/VisibleRefFilter.java
index ed86a92..deede71 100644
--- a/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.git;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CACHE_AUTOMERGE;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
@@ -23,6 +24,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
@@ -30,6 +32,8 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
import com.google.gerrit.server.permissions.ChangePermission;
@@ -75,6 +79,7 @@
@Nullable private final SearchingChangeCacheImpl changeCache;
private final Provider<ReviewDb> db;
private final Provider<CurrentUser> user;
+ private final GroupCache groupCache;
private final PermissionBackend permissionBackend;
private final PermissionBackend.ForProject perm;
private final ProjectState projectState;
@@ -90,6 +95,7 @@
@Nullable SearchingChangeCacheImpl changeCache,
Provider<ReviewDb> db,
Provider<CurrentUser> user,
+ GroupCache groupCache,
PermissionBackend permissionBackend,
@Assisted ProjectState projectState,
@Assisted Repository git) {
@@ -98,6 +104,7 @@
this.changeCache = changeCache;
this.db = db;
this.user = user;
+ this.groupCache = groupCache;
this.permissionBackend = permissionBackend;
this.perm =
permissionBackend.user(user).database(db).project(projectState.getProject().getNameKey());
@@ -118,22 +125,29 @@
PermissionBackend.WithUser withUser = permissionBackend.user(user);
PermissionBackend.ForProject forProject = withUser.project(projectState.getNameKey());
- if (checkProjectPermission(forProject, ProjectPermission.READ)) {
- return refs;
- } else if (checkProjectPermission(forProject, ProjectPermission.READ_NO_CONFIG)) {
- return fastHideRefsMetaConfig(refs);
+ if (!projectState.isAllUsers()) {
+ if (checkProjectPermission(forProject, ProjectPermission.READ)) {
+ return refs;
+ } else if (checkProjectPermission(forProject, ProjectPermission.READ_NO_CONFIG)) {
+ return fastHideRefsMetaConfig(refs);
+ }
}
- Account.Id userId;
boolean viewMetadata;
+ boolean isAdmin;
+ Account.Id userId;
+ IdentifiedUser identifiedUser;
if (user.get().isIdentifiedUser()) {
viewMetadata = withUser.testOrFalse(GlobalPermission.ACCESS_DATABASE);
- IdentifiedUser u = user.get().asIdentifiedUser();
- userId = u.getAccountId();
+ isAdmin = withUser.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
+ identifiedUser = user.get().asIdentifiedUser();
+ userId = identifiedUser.getAccountId();
userEditPrefix = RefNames.refsEditPrefix(userId);
} else {
- userId = null;
viewMetadata = false;
+ isAdmin = false;
+ userId = null;
+ identifiedUser = null;
}
Map<String, Ref> result = new HashMap<>();
@@ -143,6 +157,7 @@
String name = ref.getName();
Change.Id changeId;
Account.Id accountId;
+ AccountGroup.UUID accountGroupUuid;
if (name.startsWith(REFS_CACHE_AUTOMERGE) || (!showMetadata && isMetadata(name))) {
continue;
} else if (RefNames.isRefsEdit(name)) {
@@ -156,10 +171,19 @@
result.put(name, ref);
}
} else if ((accountId = Account.Id.fromRef(name)) != null) {
- // Account ref is visible only to corresponding account.
+ // Account ref is visible only to the corresponding account.
if (viewMetadata || (accountId.equals(userId) && canReadRef(name))) {
result.put(name, ref);
}
+ } else if ((accountGroupUuid = AccountGroup.UUID.fromRef(name)) != null) {
+ // Group ref is visible only to the corresponding owner group.
+ InternalGroup group = groupCache.get(accountGroupUuid).orElse(null);
+ if (viewMetadata
+ || (group != null
+ && isGroupOwner(group, identifiedUser, isAdmin)
+ && canReadRef(name))) {
+ result.put(name, ref);
+ }
} else if (isTag(ref)) {
// If its a tag, consider it later.
if (ref.getObjectId() != null) {
@@ -170,8 +194,10 @@
if (viewMetadata) {
result.put(name, ref);
}
- } else if (projectState.isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS)) {
- // The notes branch with the external IDs of all users must not be exposed to normal users.
+ } else if (projectState.isAllUsers()
+ && (name.equals(RefNames.REFS_EXTERNAL_IDS) || name.equals(RefNames.REFS_GROUPNAMES))) {
+ // The notes branches with the external IDs / group names must not be exposed to normal
+ // users.
if (viewMetadata) {
result.put(name, ref);
}
@@ -180,6 +206,12 @@
// symbolic we want the control around the final target. If its
// not symbolic then getLeaf() is a no-op returning ref itself.
result.put(name, ref);
+ } else if (isRefsUsersSelf(ref)) {
+ // viewMetadata allows to see all account refs, hence refs/users/self should be included as
+ // well
+ if (viewMetadata) {
+ result.put(name, ref);
+ }
}
}
@@ -327,15 +359,17 @@
}
private boolean isMetadata(String name) {
- return name.startsWith(REFS_CHANGES)
- || RefNames.isRefsEdit(name)
- || (projectState.isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS));
+ return name.startsWith(REFS_CHANGES) || RefNames.isRefsEdit(name);
}
private static boolean isTag(Ref ref) {
return ref.getLeaf().getName().startsWith(Constants.R_TAGS);
}
+ private static boolean isRefsUsersSelf(Ref ref) {
+ return ref.getName().startsWith(REFS_USERS_SELF);
+ }
+
private boolean canReadRef(String ref) {
try {
perm.ref(ref).check(RefPermission.READ);
@@ -364,4 +398,13 @@
}
return true;
}
+
+ private boolean isGroupOwner(
+ InternalGroup group, @Nullable IdentifiedUser user, boolean isAdmin) {
+ checkNotNull(group);
+
+ // Keep this logic in sync with GroupControl#isOwner().
+ return isAdmin
+ || (user != null && user.getEffectiveGroups().contains(group.getOwnerGroupUUID()));
+ }
}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 417bd26..b9af127 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -778,7 +778,7 @@
}
}
- /** Rejects updates to group branches. */
+ /** Rejects updates to group branches (refs/groups/* and refs/meta/group-names). */
public static class GroupCommitValidator implements CommitValidationListener {
private final AllUsersName allUsers;
@@ -790,6 +790,7 @@
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
// Groups are stored inside 'refs/groups/' refs inside the 'All-Users' repository.
+ // Group names are stored inside 'refs/meta/group-names' refs inside the 'All-Users' repository.
if (!allUsers.equals(receiveEvent.project.getNameKey())) {
return Collections.emptyList();
}
@@ -800,7 +801,8 @@
return Collections.emptyList();
}
- if (receiveEvent.command.getRefName().startsWith(RefNames.REFS_GROUPS)) {
+ if (receiveEvent.command.getRefName().startsWith(RefNames.REFS_GROUPS)
+ || receiveEvent.command.getRefName().equals(RefNames.REFS_GROUPNAMES)) {
throw new CommitValidationException("group update not allowed");
}
return Collections.emptyList();
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index ee6f387..22335b6 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -319,8 +319,10 @@
IdentifiedUser caller)
throws MergeValidationException {
// Groups are stored inside 'refs/groups/' refs inside the 'All-Users' repository.
+ // Group names are stored inside 'refs/meta/group-names' inside the 'All-Users' repository.
if (!allUsersName.equals(destProject.getNameKey())
- || !destBranch.get().startsWith(RefNames.REFS_GROUPS)) {
+ || (!destBranch.get().startsWith(RefNames.REFS_GROUPS)
+ && !destBranch.get().equals(RefNames.REFS_GROUPNAMES))) {
return;
}
diff --git a/java/com/google/gerrit/server/group/GetAuditLog.java b/java/com/google/gerrit/server/group/GetAuditLog.java
index ebada0b..58a057b 100644
--- a/java/com/google/gerrit/server/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/group/GetAuditLog.java
@@ -36,10 +36,12 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class GetAuditLog implements RestReadView<GroupResource> {
@@ -68,7 +70,8 @@
@Override
public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
- throws AuthException, MethodNotAllowedException, OrmException {
+ throws AuthException, MethodNotAllowedException, OrmException, IOException,
+ ConfigInvalidException {
GroupDescription.Internal group =
rsrc.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
if (!rsrc.getControl().isOwner()) {
@@ -123,8 +126,9 @@
accountLoader.fill();
- // sort by date in reverse order so that the newest audit event comes first
- Collections.sort(auditEvents, comparing((GroupAuditEventInfo a) -> a.date).reversed());
+ // sort by date and then reverse so that the newest audit event comes first
+ Collections.sort(auditEvents, comparing((GroupAuditEventInfo a) -> a.date));
+ Collections.reverse(auditEvents);
return auditEvents;
}
diff --git a/java/com/google/gerrit/server/group/InternalGroup.java b/java/com/google/gerrit/server/group/InternalGroup.java
index ad6fa5c..7828586 100644
--- a/java/com/google/gerrit/server/group/InternalGroup.java
+++ b/java/com/google/gerrit/server/group/InternalGroup.java
@@ -21,6 +21,7 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import java.io.Serializable;
import java.sql.Timestamp;
+import org.eclipse.jgit.lib.ObjectId;
@AutoValue
public abstract class InternalGroup implements Serializable {
@@ -30,6 +31,14 @@
AccountGroup accountGroup,
ImmutableSet<Account.Id> members,
ImmutableSet<AccountGroup.UUID> subgroups) {
+ return create(accountGroup, members, subgroups, null);
+ }
+
+ public static InternalGroup create(
+ AccountGroup accountGroup,
+ ImmutableSet<Account.Id> members,
+ ImmutableSet<AccountGroup.UUID> subgroups,
+ ObjectId refState) {
return builder()
.setId(accountGroup.getId())
.setNameKey(accountGroup.getNameKey())
@@ -40,6 +49,7 @@
.setCreatedOn(accountGroup.getCreatedOn())
.setMembers(members)
.setSubgroups(subgroups)
+ .setRefState(refState)
.build();
}
@@ -66,6 +76,11 @@
public abstract ImmutableSet<AccountGroup.UUID> getSubgroups();
+ @Nullable
+ public abstract ObjectId getRefState();
+
+ public abstract Builder toBuilder();
+
public static Builder builder() {
return new AutoValue_InternalGroup.Builder();
}
@@ -90,6 +105,8 @@
public abstract Builder setSubgroups(ImmutableSet<AccountGroup.UUID> subgroups);
+ public abstract Builder setRefState(ObjectId refState);
+
public abstract InternalGroup build();
}
}
diff --git a/java/com/google/gerrit/server/group/ListMembers.java b/java/com/google/gerrit/server/group/ListMembers.java
index 2d069f8..fa33957 100644
--- a/java/com/google/gerrit/server/group/ListMembers.java
+++ b/java/com/google/gerrit/server/group/ListMembers.java
@@ -26,10 +26,10 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.api.accounts.AccountInfoComparator;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.util.ArrayList;
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
new file mode 100644
index 0000000..3ab91dd
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -0,0 +1,274 @@
+// Copyright (C) 2017 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.group.db;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.NoteDbUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** NoteDb reader for group audit log. */
+@Singleton
+class AuditLogReader {
+ private static final Logger log = LoggerFactory.getLogger(AuditLogReader.class);
+
+ private final String serverId;
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsers;
+
+ @Inject
+ AuditLogReader(
+ @GerritServerId String serverId, GitRepositoryManager repoManager, AllUsersName allUsers) {
+ this.serverId = serverId;
+ this.repoManager = repoManager;
+ this.allUsers = allUsers;
+ }
+
+ // Having separate methods for reading the two types of audit records mirrors the split in
+ // ReviewDb. Once ReviewDb is gone, the audit record interface becomes more flexible and we can
+ // revisit this, e.g. to do only a single walk, or even change the record types.
+
+ ImmutableList<AccountGroupMemberAudit> getMembersAudit(AccountGroup.UUID uuid)
+ throws IOException, ConfigInvalidException {
+ return new MembersAuditLogParser().parseAuditLog(uuid);
+ }
+
+ ImmutableList<AccountGroupByIdAud> getSubgroupsAudit(AccountGroup.UUID uuid)
+ throws IOException, ConfigInvalidException {
+ return new SubgroupsAuditLogParser().parseAuditLog(uuid);
+ }
+
+ private Optional<ParsedCommit> parse(AccountGroup.UUID uuid, RevCommit c) {
+ Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent(), serverId);
+ if (!authorId.isPresent()) {
+ // Only report audit events from identified users, since this is a non-nullable field in
+ // ReviewDb. May be revisited after groups are fully migrated to NoteDb.
+ return Optional.empty();
+ }
+
+ List<Account.Id> addedMembers = new ArrayList<>();
+ List<AccountGroup.UUID> addedSubgroups = new ArrayList<>();
+ List<Account.Id> removedMembers = new ArrayList<>();
+ List<AccountGroup.UUID> removedSubgroups = new ArrayList<>();
+
+ for (FooterLine line : c.getFooterLines()) {
+ if (line.matches(GroupConfig.FOOTER_ADD_MEMBER)) {
+ parseAccount(uuid, c, line).ifPresent(addedMembers::add);
+ } else if (line.matches(GroupConfig.FOOTER_REMOVE_MEMBER)) {
+ parseAccount(uuid, c, line).ifPresent(removedMembers::add);
+ } else if (line.matches(GroupConfig.FOOTER_ADD_GROUP)) {
+ parseGroup(uuid, c, line).ifPresent(addedSubgroups::add);
+ } else if (line.matches(GroupConfig.FOOTER_REMOVE_GROUP)) {
+ parseGroup(uuid, c, line).ifPresent(removedSubgroups::add);
+ }
+ }
+ return Optional.of(
+ new AutoValue_AuditLogReader_ParsedCommit(
+ authorId.get(),
+ new Timestamp(c.getAuthorIdent().getWhen().getTime()),
+ ImmutableList.copyOf(addedMembers),
+ ImmutableList.copyOf(removedMembers),
+ ImmutableList.copyOf(addedSubgroups),
+ ImmutableList.copyOf(removedSubgroups)));
+ }
+
+ private Optional<Account.Id> parseAccount(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
+ Optional<Account.Id> result =
+ Optional.ofNullable(RawParseUtils.parsePersonIdent(line.getValue()))
+ .flatMap(ident -> NoteDbUtil.parseIdent(ident, serverId));
+ if (!result.isPresent()) {
+ logInvalid(uuid, c, line);
+ }
+ return result;
+ }
+
+ private static Optional<AccountGroup.UUID> parseGroup(
+ AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
+ PersonIdent ident = RawParseUtils.parsePersonIdent(line.getValue());
+ if (ident == null) {
+ logInvalid(uuid, c, line);
+ return Optional.empty();
+ }
+ return Optional.of(new AccountGroup.UUID(ident.getEmailAddress()));
+ }
+
+ private static void logInvalid(AccountGroup.UUID uuid, RevCommit c, FooterLine line) {
+ log.debug(
+ "Invalid footer line in commit {} while parsing audit log for group {}: {}",
+ c.name(),
+ uuid,
+ line);
+ }
+
+ private abstract class AuditLogParser<T> {
+ final ImmutableList<T> parseAuditLog(AccountGroup.UUID uuid)
+ throws IOException, ConfigInvalidException {
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ Ref ref = repo.exactRef(RefNames.refsGroups(uuid));
+ if (ref == null) {
+ return ImmutableList.of();
+ }
+
+ // TODO(dborowitz): This re-walks all commits just to find createdOn, which we don't need.
+ AccountGroup.Id groupId =
+ GroupConfig.loadForGroup(repo, uuid).getLoadedGroup().get().getId();
+
+ rw.reset();
+ rw.markStart(rw.parseCommit(ref.getObjectId()));
+ rw.setRetainBody(true);
+ rw.sort(RevSort.COMMIT_TIME_DESC, true);
+ rw.sort(RevSort.REVERSE, true);
+
+ ImmutableList.Builder<T> result = ImmutableList.builder();
+ RevCommit c;
+ while ((c = rw.next()) != null) {
+ parse(uuid, c).ifPresent(pc -> visit(groupId, pc, result));
+ }
+ return result.build();
+ }
+ }
+
+ protected abstract void visit(
+ AccountGroup.Id groupId, ParsedCommit pc, ImmutableList.Builder<T> result);
+ }
+
+ private class MembersAuditLogParser extends AuditLogParser<AccountGroupMemberAudit> {
+ private ListMultimap<MemberKey, AccountGroupMemberAudit> audits =
+ MultimapBuilder.hashKeys().linkedListValues().build();
+
+ @Override
+ protected void visit(
+ AccountGroup.Id groupId,
+ ParsedCommit pc,
+ ImmutableList.Builder<AccountGroupMemberAudit> result) {
+ for (Account.Id id : pc.addedMembers()) {
+ MemberKey key = MemberKey.create(groupId, id);
+ AccountGroupMemberAudit audit =
+ new AccountGroupMemberAudit(
+ new AccountGroupMemberAudit.Key(id, groupId, pc.when()), pc.authorId());
+ audits.put(key, audit);
+ result.add(audit);
+ }
+ for (Account.Id id : pc.removedMembers()) {
+ List<AccountGroupMemberAudit> adds = audits.get(MemberKey.create(groupId, id));
+ if (!adds.isEmpty()) {
+ AccountGroupMemberAudit audit = adds.remove(0);
+ audit.removed(pc.authorId(), pc.when());
+ } else {
+ // Match old behavior of DbGroupMemberAuditListener and add a "legacy" add/remove pair.
+ AccountGroupMemberAudit audit =
+ new AccountGroupMemberAudit(
+ new AccountGroupMemberAudit.Key(id, groupId, pc.when()), pc.authorId());
+ audit.removedLegacy();
+ result.add(audit);
+ }
+ }
+ }
+ }
+
+ private class SubgroupsAuditLogParser extends AuditLogParser<AccountGroupByIdAud> {
+ private ListMultimap<SubgroupKey, AccountGroupByIdAud> audits =
+ MultimapBuilder.hashKeys().linkedListValues().build();
+
+ @Override
+ protected void visit(
+ AccountGroup.Id groupId,
+ ParsedCommit pc,
+ ImmutableList.Builder<AccountGroupByIdAud> result) {
+ for (AccountGroup.UUID uuid : pc.addedSubgroups()) {
+ SubgroupKey key = SubgroupKey.create(groupId, uuid);
+ AccountGroupByIdAud audit =
+ new AccountGroupByIdAud(
+ new AccountGroupByIdAud.Key(groupId, uuid, pc.when()), pc.authorId());
+ audits.put(key, audit);
+ result.add(audit);
+ }
+ for (AccountGroup.UUID uuid : pc.removedSubgroups()) {
+ List<AccountGroupByIdAud> adds = audits.get(SubgroupKey.create(groupId, uuid));
+ if (!adds.isEmpty()) {
+ AccountGroupByIdAud audit = adds.remove(0);
+ audit.removed(pc.authorId(), pc.when());
+ } else {
+ // Unlike members, DbGroupMemberAuditListener didn't insert an add/remove pair here.
+ }
+ }
+ }
+ }
+
+ @AutoValue
+ abstract static class MemberKey {
+ static MemberKey create(AccountGroup.Id groupId, Account.Id memberId) {
+ return new AutoValue_AuditLogReader_MemberKey(groupId, memberId);
+ }
+
+ abstract AccountGroup.Id groupId();
+
+ abstract Account.Id memberId();
+ }
+
+ @AutoValue
+ abstract static class SubgroupKey {
+ static SubgroupKey create(AccountGroup.Id groupId, AccountGroup.UUID subgroupUuid) {
+ return new AutoValue_AuditLogReader_SubgroupKey(groupId, subgroupUuid);
+ }
+
+ abstract AccountGroup.Id groupId();
+
+ abstract AccountGroup.UUID subgroupUuid();
+ }
+
+ @AutoValue
+ abstract static class ParsedCommit {
+ abstract Account.Id authorId();
+
+ abstract Timestamp when();
+
+ abstract ImmutableList<Account.Id> addedMembers();
+
+ abstract ImmutableList<Account.Id> removedMembers();
+
+ abstract ImmutableList<AccountGroup.UUID> addedSubgroups();
+
+ abstract ImmutableList<AccountGroup.UUID> removedSubgroups();
+ }
+}
diff --git a/java/com/google/gerrit/server/group/db/GroupBundle.java b/java/com/google/gerrit/server/group/db/GroupBundle.java
index f83f094..2f3b118 100644
--- a/java/com/google/gerrit/server/group/db/GroupBundle.java
+++ b/java/com/google/gerrit/server/group/db/GroupBundle.java
@@ -14,11 +14,11 @@
package com.google.gerrit.server.group.db;
-import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns;
import com.google.auto.value.AutoValue;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupById;
@@ -28,6 +28,11 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Repository;
/**
* A bundle of all entities rooted at a single {@link AccountGroup} entity.
@@ -37,17 +42,84 @@
*/
@AutoValue
public abstract class GroupBundle {
- public static GroupBundle fromReviewDb(ReviewDb db, AccountGroup.Id id) throws OrmException {
- AccountGroup group = db.accountGroups().get(id);
- if (group == null) {
- throw new OrmException("Group " + id + " not found");
+ static {
+ // Initialization-time checks that the column set hasn't changed since the
+ // last time this file was updated.
+ checkColumns(AccountGroup.NameKey.class, 1);
+ checkColumns(AccountGroup.UUID.class, 1);
+ checkColumns(AccountGroup.Id.class, 1);
+ checkColumns(AccountGroup.class, 1, 2, 4, 7, 9, 10, 11);
+
+ checkColumns(AccountGroupById.Key.class, 1, 2);
+ checkColumns(AccountGroupById.class, 1);
+
+ checkColumns(AccountGroupByIdAud.Key.class, 1, 2, 3);
+ checkColumns(AccountGroupByIdAud.class, 1, 2, 3, 4);
+
+ checkColumns(AccountGroupMember.Key.class, 1, 2);
+ checkColumns(AccountGroupMember.class, 1);
+
+ checkColumns(AccountGroupMemberAudit.Key.class, 1, 2, 3);
+ checkColumns(AccountGroupMemberAudit.class, 1, 2, 3, 4);
+ }
+
+ @Singleton
+ public static class Factory {
+ private final AuditLogReader auditLogReader;
+
+ @Inject
+ Factory(AuditLogReader auditLogReader) {
+ this.auditLogReader = auditLogReader;
}
- return create(
- group,
- db.accountGroupMembers().byGroup(id),
- db.accountGroupMembersAudit().byGroup(id),
- db.accountGroupById().byGroup(id),
- db.accountGroupByIdAud().byGroup(id));
+
+ public GroupBundle fromReviewDb(ReviewDb db, AccountGroup.Id id) throws OrmException {
+ AccountGroup group = db.accountGroups().get(id);
+ if (group == null) {
+ throw new OrmException("Group " + id + " not found");
+ }
+ return create(
+ group,
+ db.accountGroupMembers().byGroup(id),
+ db.accountGroupMembersAudit().byGroup(id),
+ db.accountGroupById().byGroup(id),
+ db.accountGroupByIdAud().byGroup(id));
+ }
+
+ public GroupBundle fromNoteDb(Repository repo, AccountGroup.UUID uuid)
+ throws ConfigInvalidException, IOException {
+ GroupConfig groupConfig = GroupConfig.loadForGroup(repo, uuid);
+ InternalGroup internalGroup = groupConfig.getLoadedGroup().get();
+ AccountGroup.Id groupId = internalGroup.getId();
+
+ AccountGroup accountGroup =
+ new AccountGroup(
+ internalGroup.getNameKey(),
+ internalGroup.getId(),
+ internalGroup.getGroupUUID(),
+ internalGroup.getCreatedOn());
+ accountGroup.setDescription(internalGroup.getDescription());
+ accountGroup.setOwnerGroupUUID(internalGroup.getOwnerGroupUUID());
+ accountGroup.setVisibleToAll(internalGroup.isVisibleToAll());
+
+ return create(
+ accountGroup,
+ internalGroup
+ .getMembers()
+ .stream()
+ .map(
+ accountId ->
+ new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId)))
+ .collect(toImmutableSet()),
+ auditLogReader.getMembersAudit(uuid),
+ internalGroup
+ .getSubgroups()
+ .stream()
+ .map(
+ subgroupUuid ->
+ new AccountGroupById(new AccountGroupById.Key(groupId, subgroupUuid)))
+ .collect(toImmutableSet()),
+ auditLogReader.getSubgroupsAudit(uuid));
+ }
}
public static GroupBundle create(
@@ -79,13 +151,13 @@
public abstract AccountGroup group();
- public abstract ImmutableList<AccountGroupMember> members();
+ public abstract ImmutableSet<AccountGroupMember> members();
- public abstract ImmutableList<AccountGroupMemberAudit> memberAudit();
+ public abstract ImmutableSet<AccountGroupMemberAudit> memberAudit();
- public abstract ImmutableList<AccountGroupById> byId();
+ public abstract ImmutableSet<AccountGroupById> byId();
- public abstract ImmutableList<AccountGroupByIdAud> byIdAudit();
+ public abstract ImmutableSet<AccountGroupByIdAud> byIdAudit();
public abstract Builder toBuilder();
@@ -97,8 +169,8 @@
return toBuilder()
.group(newGroup)
.memberAudit(
- memberAudit().stream().map(GroupBundle::roundToSecond).collect(toImmutableList()))
- .byIdAudit(byIdAudit().stream().map(GroupBundle::roundToSecond).collect(toImmutableList()))
+ memberAudit().stream().map(GroupBundle::roundToSecond).collect(toImmutableSet()))
+ .byIdAudit(byIdAudit().stream().map(GroupBundle::roundToSecond).collect(toImmutableSet()))
.build();
}
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index 8ac533c..c23dc09 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -25,6 +25,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gwtorm.server.OrmDuplicateKeyException;
@@ -40,14 +41,22 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
// TODO(aliceks): Add Javadoc descriptions to this file.
public class GroupConfig extends VersionedMetaData {
- private static final String GROUP_CONFIG_FILE = "group.config";
+ public static final String GROUP_CONFIG_FILE = "group.config";
+
+ static final FooterKey FOOTER_ADD_MEMBER = new FooterKey("Add");
+ static final FooterKey FOOTER_REMOVE_MEMBER = new FooterKey("Remove");
+ static final FooterKey FOOTER_ADD_GROUP = new FooterKey("Add-group");
+ static final FooterKey FOOTER_REMOVE_GROUP = new FooterKey("Remove-group");
+
private static final String MEMBERS_FILE = "members";
private static final String SUBGROUPS_FILE = "subgroups";
private static final Pattern LINE_SEPARATOR_PATTERN = Pattern.compile("\\R");
@@ -123,7 +132,9 @@
Config config = readConfig(GROUP_CONFIG_FILE);
ImmutableSet<Account.Id> members = readMembers();
ImmutableSet<AccountGroup.UUID> subgroups = readSubgroups();
- loadedGroup = Optional.of(createFrom(groupUuid, config, members, subgroups, createdOn));
+ loadedGroup =
+ Optional.of(
+ createFrom(groupUuid, config, members, subgroups, createdOn, revision.toObjectId()));
}
isLoaded = true;
@@ -134,7 +145,8 @@
Config config,
ImmutableSet<Account.Id> members,
ImmutableSet<AccountGroup.UUID> subgroups,
- Timestamp createdOn) {
+ Timestamp createdOn,
+ ObjectId refState) {
InternalGroup.Builder group = InternalGroup.builder();
group.setGroupUUID(groupUuid);
Arrays.stream(GroupConfigEntry.values())
@@ -142,10 +154,18 @@
group.setMembers(members);
group.setSubgroups(subgroups);
group.setCreatedOn(createdOn);
+ group.setRefState(refState);
return group.build();
}
@Override
+ public RevCommit commit(MetaDataUpdate update) throws IOException {
+ RevCommit c = super.commit(update);
+ loadedGroup = Optional.of(loadedGroup.get().toBuilder().setRefState(c.toObjectId()).build());
+ return c;
+ }
+
+ @Override
protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
checkLoaded();
if (!groupCreation.isPresent() && !groupUpdate.isPresent()) {
@@ -184,7 +204,8 @@
config,
updatedMembers.orElse(originalMembers),
updatedSubgroups.orElse(originalSubgroups),
- createdOn));
+ createdOn,
+ null));
groupCreation = Optional.empty();
return true;
@@ -318,12 +339,12 @@
Sets.difference(oldMembers, newMembers)
.stream()
.map(accountNameEmailRetriever)
- .map("Remove: "::concat);
+ .map((FOOTER_REMOVE_MEMBER.getName() + ": ")::concat);
Stream<String> addedMembers =
Sets.difference(newMembers, oldMembers)
.stream()
.map(accountNameEmailRetriever)
- .map("Add: "::concat);
+ .map((FOOTER_ADD_MEMBER.getName() + ": ")::concat);
return Stream.concat(removedMembers, addedMembers);
}
@@ -333,12 +354,12 @@
Sets.difference(oldSubgroups, newSubgroups)
.stream()
.map(groupNameRetriever)
- .map("Remove-group: "::concat);
+ .map((FOOTER_REMOVE_GROUP.getName() + ": ")::concat);
Stream<String> addedMembers =
Sets.difference(newSubgroups, oldSubgroups)
.stream()
.map(groupNameRetriever)
- .map("Add-group: "::concat);
+ .map((FOOTER_ADD_GROUP.getName() + ": ")::concat);
return Stream.concat(removedMembers, addedMembers);
}
}
diff --git a/java/com/google/gerrit/server/group/db/GroupNameNotes.java b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
index cd8effd..ee7f849 100644
--- a/java/com/google/gerrit/server/group/db/GroupNameNotes.java
+++ b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
@@ -15,9 +15,12 @@
package com.google.gerrit.server.group.db;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableBiMap.toImmutableBiMap;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.Hashing;
import com.google.gerrit.common.Nullable;
@@ -27,20 +30,25 @@
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
// TODO(aliceks): Add Javadoc descriptions.
public class GroupNameNotes extends VersionedMetaData {
@@ -48,6 +56,63 @@
private static final String UUID_PARAM = "uuid";
private static final String NAME_PARAM = "name";
+ @VisibleForTesting
+ static final String UNIQUE_REF_ERROR = "GroupReference collection must contain unique references";
+
+ public static void updateGroupNames(
+ Repository allUsersRepo,
+ ObjectInserter inserter,
+ BatchRefUpdate bru,
+ Collection<GroupReference> groupReferences,
+ PersonIdent ident)
+ throws IOException {
+ // Not strictly necessary for iteration; throws IAE if it encounters duplicates, which is nice.
+ ImmutableBiMap<AccountGroup.UUID, String> biMap = toBiMap(groupReferences);
+
+ try (ObjectReader reader = inserter.newReader();
+ RevWalk rw = new RevWalk(reader)) {
+ // Always start from an empty map, discarding old notes.
+ NoteMap noteMap = NoteMap.newEmptyMap();
+ Ref ref = allUsersRepo.exactRef(RefNames.REFS_GROUPNAMES);
+ RevCommit oldCommit = ref != null ? rw.parseCommit(ref.getObjectId()) : null;
+
+ for (Map.Entry<AccountGroup.UUID, String> e : biMap.entrySet()) {
+ AccountGroup.NameKey nameKey = new AccountGroup.NameKey(e.getValue());
+ ObjectId noteKey = getNoteKey(nameKey);
+ noteMap.set(noteKey, getAsNoteData(e.getKey(), nameKey), inserter);
+ }
+
+ ObjectId newTreeId = noteMap.writeTree(inserter);
+ if (oldCommit != null && newTreeId.equals(oldCommit.getTree())) {
+ return;
+ }
+ CommitBuilder cb = new CommitBuilder();
+ if (oldCommit != null) {
+ cb.addParentId(oldCommit);
+ }
+ cb.setTreeId(newTreeId);
+ cb.setAuthor(ident);
+ cb.setCommitter(ident);
+ int n = groupReferences.size();
+ cb.setMessage("Store " + n + " group name" + (n != 1 ? "s" : ""));
+ ObjectId newId = inserter.insert(cb).copy();
+
+ ObjectId oldId = oldCommit != null ? oldCommit.copy() : ObjectId.zeroId();
+ bru.addCommand(new ReceiveCommand(oldId, newId, RefNames.REFS_GROUPNAMES));
+ }
+ }
+
+ private static ImmutableBiMap<AccountGroup.UUID, String> toBiMap(
+ Collection<GroupReference> groupReferences) {
+ try {
+ return groupReferences
+ .stream()
+ .collect(toImmutableBiMap(gr -> gr.getUUID(), gr -> gr.getName()));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(UNIQUE_REF_ERROR, e);
+ }
+ }
+
private final AccountGroup.UUID groupUuid;
private final Optional<AccountGroup.NameKey> oldGroupName;
private final Optional<AccountGroup.NameKey> newGroupName;
@@ -175,6 +240,7 @@
}
commit.setTreeId(noteMap.writeTree(inserter));
+ commit.setMessage(getCommitMessage());
return true;
}
@@ -197,7 +263,8 @@
// Use the same approach as ExternalId.Key.sha1().
@SuppressWarnings("deprecation")
- private static ObjectId getNoteKey(AccountGroup.NameKey groupName) {
+ @VisibleForTesting
+ static ObjectId getNoteKey(AccountGroup.NameKey groupName) {
return ObjectId.fromRaw(Hashing.sha1().hashString(groupName.get(), UTF_8).asBytes());
}
@@ -208,7 +275,8 @@
return config.toText();
}
- private static GroupReference getGroupReference(ObjectReader reader, ObjectId noteDataBlobId)
+ @VisibleForTesting
+ public static GroupReference getGroupReference(ObjectReader reader, ObjectId noteDataBlobId)
throws IOException, ConfigInvalidException {
byte[] noteData = reader.open(noteDataBlobId, OBJ_BLOB).getCachedBytes();
return getFromNoteData(noteData);
@@ -227,4 +295,18 @@
return new GroupReference(new AccountGroup.UUID(uuid), name);
}
+
+ private String getCommitMessage() {
+ if (oldGroupName.isPresent() && newGroupName.isPresent()) {
+ return String.format(
+ "Rename group from '%s' to '%s'", oldGroupName.get(), newGroupName.get());
+ }
+ if (newGroupName.isPresent()) {
+ return String.format("Create group '%s'", newGroupName.get());
+ }
+ if (oldGroupName.isPresent()) {
+ return String.format("Delete group '%s'", oldGroupName.get());
+ }
+ return "No-op";
+ }
}
diff --git a/java/com/google/gerrit/server/group/db/GroupRebuilder.java b/java/com/google/gerrit/server/group/db/GroupRebuilder.java
index b5301e5..7f8d8a9 100644
--- a/java/com/google/gerrit/server/group/db/GroupRebuilder.java
+++ b/java/com/google/gerrit/server/group/db/GroupRebuilder.java
@@ -15,14 +15,18 @@
package com.google.gerrit.server.group.db;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
@@ -46,11 +50,14 @@
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
+import java.util.NavigableSet;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
@@ -107,7 +114,7 @@
this.getGroupNameFunc = getGroupNameFunc;
}
- public void rebuild(Repository allUsersRepo, GroupBundle bundle)
+ public void rebuild(Repository allUsersRepo, GroupBundle bundle, @Nullable BatchRefUpdate bru)
throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersRepo, bundle.uuid());
AccountGroup group = bundle.group();
@@ -128,10 +135,12 @@
}
groupConfig.setGroupUpdate(updateBuilder.build(), getAccountNameEmailFunc, getGroupNameFunc);
- MetaDataUpdate md = metaDataUpdateFactory.create(allUsers, allUsersRepo, null);
+ Map<Key, Collection<Event>> events = toEvents(bundle).asMap();
+ PersonIdent nowServerIdent = getServerIdent(events);
+
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers, allUsersRepo, bru);
// Creation is done by the server (unlike later audit events).
- PersonIdent nowServerIdent = serverIdent.get();
PersonIdent created = new PersonIdent(nowServerIdent, group.getCreatedOn());
md.getCommitBuilder().setAuthor(created);
md.getCommitBuilder().setCommitter(created);
@@ -140,7 +149,6 @@
try (BatchMetaDataUpdate batch = groupConfig.openUpdate(md)) {
batch.write(groupConfig, md.getCommitBuilder());
- Map<Key, Collection<Event>> events = toEvents(bundle, nowServerIdent).asMap();
for (Map.Entry<Key, Collection<Event>> e : events.entrySet()) {
InternalGroupUpdate.Builder ub = InternalGroupUpdate.builder();
e.getValue().forEach(event -> event.update().accept(ub));
@@ -161,7 +169,7 @@
}
}
- private ListMultimap<Key, Event> toEvents(GroupBundle bundle, PersonIdent nowServerIdent) {
+ private ListMultimap<Key, Event> toEvents(GroupBundle bundle) {
ListMultimap<Key, Event> result =
MultimapBuilder.treeKeys(Key.COMPARATOR).arrayListValues(1).build();
Event e;
@@ -196,13 +204,32 @@
}
}
- Timestamp now = new Timestamp(nowServerIdent.getWhen().getTime());
- e = serverEvent(Type.FIXUP, now, setCurrentMembership(bundle));
+ // Due to clock skew, audit events may be in the future relative to this machine. Ensure the
+ // fixup event happens after any other events, both for the purposes of sorting Keys correctly
+ // and to avoid non-monotonic timestamps in the commit history.
+ Timestamp maxTs =
+ Stream.concat(result.keySet().stream().map(Key::when), Stream.of(TimeUtil.nowTs()))
+ .max(Comparator.naturalOrder())
+ .get();
+ Timestamp fixupTs = new Timestamp(maxTs.getTime() + 1);
+ e = serverEvent(Type.FIXUP, fixupTs, setCurrentMembership(bundle));
result.put(e.key(), e);
return result;
}
+ private PersonIdent getServerIdent(Map<Key, Collection<Event>> events) {
+ // Created with MultimapBuilder.treeKeys, so the keySet is navigable.
+ Key lastKey = ((NavigableSet<Key>) events.keySet()).last();
+ checkState(lastKey.type() == Type.FIXUP);
+ PersonIdent ident = serverIdent.get();
+ return new PersonIdent(
+ ident.getName(),
+ ident.getEmailAddress(),
+ Iterables.getOnlyElement(events.get(lastKey)).when(),
+ ident.getTimeZone());
+ }
+
private static Consumer<InternalGroupUpdate.Builder> addMember(Account.Id toAdd) {
return b -> {
MemberModification prev = b.getMemberModification();
diff --git a/java/com/google/gerrit/server/group/db/Groups.java b/java/com/google/gerrit/server/group/db/Groups.java
index 0260f41..7f63021 100644
--- a/java/com/google/gerrit/server/group/db/Groups.java
+++ b/java/com/google/gerrit/server/group/db/Groups.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.group.db;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static java.util.Comparator.comparing;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -30,20 +31,20 @@
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
/**
@@ -61,18 +62,21 @@
*/
@Singleton
public class Groups {
- private final boolean readFromNoteDb;
+ private final GroupsMigration groupsMigration;
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
+ private final AuditLogReader auditLogReader;
@Inject
public Groups(
- @GerritServerConfig Config config,
+ GroupsMigration groupsMigration,
GitRepositoryManager repoManager,
- AllUsersName allUsersName) {
- readFromNoteDb = config.getBoolean("user", null, "readGroupsFromNoteDb", false);
+ AllUsersName allUsersName,
+ AuditLogReader auditLogReader) {
+ this.groupsMigration = groupsMigration;
this.repoManager = repoManager;
this.allUsersName = allUsersName;
+ this.auditLogReader = auditLogReader;
}
/**
@@ -105,7 +109,7 @@
*/
public Optional<InternalGroup> getGroup(ReviewDb db, AccountGroup.UUID groupUuid)
throws OrmException, IOException, ConfigInvalidException {
- if (readFromNoteDb) {
+ if (groupsMigration.readFromNoteDb()) {
try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
return getGroupFromNoteDb(allUsersRepo, groupUuid);
}
@@ -182,7 +186,7 @@
*/
public Stream<GroupReference> getAllGroupReferences(ReviewDb db)
throws OrmException, IOException, ConfigInvalidException {
- if (readFromNoteDb) {
+ if (groupsMigration.readFromNoteDb()) {
try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
return GroupNameNotes.loadAllGroupReferences(allUsersRepo).stream();
}
@@ -278,7 +282,7 @@
*/
public Stream<AccountGroup.UUID> getExternalGroups(ReviewDb db)
throws OrmException, IOException, ConfigInvalidException {
- if (readFromNoteDb) {
+ if (groupsMigration.readFromNoteDb()) {
try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
return getExternalGroupsFromNoteDb(allUsersRepo);
}
@@ -312,18 +316,23 @@
* @param groupUuid the UUID of the group
* @return the audit records, in arbitrary order; empty if the group does not exist
* @throws OrmException if an error occurs while reading from ReviewDb
+ * @throws IOException if an error occurs while reading from NoteDb
+ * @throws ConfigInvalidException if the group couldn't be retrieved from NoteDb
*/
public List<AccountGroupMemberAudit> getMembersAudit(ReviewDb db, AccountGroup.UUID groupUuid)
- throws OrmException {
- if (readFromNoteDb) {
- // TODO(dborowitz): Implement.
- throw new OrmException("Audit logs not yet implemented in NoteDb");
+ throws OrmException, IOException, ConfigInvalidException {
+ if (groupsMigration.readFromNoteDb()) {
+ return auditLogReader.getMembersAudit(groupUuid);
}
Optional<AccountGroup> group = getGroupFromReviewDb(db, groupUuid);
if (!group.isPresent()) {
return ImmutableList.of();
}
- return db.accountGroupMembersAudit().byGroup(group.get().getId()).toList();
+
+ List<AccountGroupMemberAudit> audits =
+ db.accountGroupMembersAudit().byGroup(group.get().getId()).toList();
+ Collections.sort(audits, comparing((AccountGroupMemberAudit a) -> a.getAddedOn()));
+ return audits;
}
/**
@@ -333,17 +342,22 @@
* @param groupUuid the UUID of the group
* @return the audit records, in arbitrary order; empty if the group does not exist
* @throws OrmException if an error occurs while reading from ReviewDb
+ * @throws IOException if an error occurs while reading from NoteDb
+ * @throws ConfigInvalidException if the group couldn't be retrieved from NoteDb
*/
public List<AccountGroupByIdAud> getSubgroupsAudit(ReviewDb db, AccountGroup.UUID groupUuid)
- throws OrmException {
- if (readFromNoteDb) {
- // TODO(dborowitz): Implement.
- throw new OrmException("Audit logs not yet implemented in NoteDb");
+ throws OrmException, IOException, ConfigInvalidException {
+ if (groupsMigration.readFromNoteDb()) {
+ return auditLogReader.getSubgroupsAudit(groupUuid);
}
Optional<AccountGroup> group = getGroupFromReviewDb(db, groupUuid);
if (!group.isPresent()) {
return ImmutableList.of();
}
- return db.accountGroupByIdAud().byGroup(group.get().getId()).toList();
+
+ List<AccountGroupByIdAud> audits =
+ db.accountGroupByIdAud().byGroup(group.get().getId()).toList();
+ Collections.sort(audits, comparing((AccountGroupByIdAud a) -> a.getAddedOn()));
+ return audits;
}
}
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 2e67ec4..80b282c 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -32,6 +32,7 @@
import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
@@ -48,6 +49,7 @@
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.RenameGroupOp;
import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
@@ -62,9 +64,9 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
/**
* A database accessor for write calls related to groups.
@@ -104,8 +106,9 @@
@Nullable private final IdentifiedUser currentUser;
private final PersonIdent authorIdent;
private final MetaDataUpdateFactory metaDataUpdateFactory;
+ private final GroupsMigration groupsMigration;
private final GitReferenceUpdated gitRefUpdated;
- private final boolean writeGroupsToNoteDb;
+ private final boolean reviewDbUpdatesAreBlocked;
@Inject
GroupsUpdate(
@@ -120,6 +123,7 @@
@GerritServerId String serverId,
@GerritPersonIdent PersonIdent serverIdent,
MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+ GroupsMigration groupsMigration,
@GerritServerConfig Config config,
GitReferenceUpdated gitRefUpdated,
@Assisted @Nullable IdentifiedUser currentUser) {
@@ -132,17 +136,14 @@
this.anonymousCowardName = anonymousCowardName;
this.renameGroupOpFactory = renameGroupOpFactory;
this.serverId = serverId;
+ this.groupsMigration = groupsMigration;
this.gitRefUpdated = gitRefUpdated;
this.currentUser = currentUser;
metaDataUpdateFactory =
getMetaDataUpdateFactory(
metaDataUpdateInternalFactory, currentUser, serverIdent, serverId, anonymousCowardName);
authorIdent = getAuthorIdent(serverIdent, currentUser);
- // TODO(aliceks): Remove this flag when all other necessary TODOs for writing groups to NoteDb
- // have been addressed.
- // Don't flip this flag in a production setting! We only added it to spread the implementation
- // of groups in NoteDb among several changes which are gradually merged.
- writeGroupsToNoteDb = config.getBoolean("user", null, "writeGroupsToNoteDb", false);
+ reviewDbUpdatesAreBlocked = config.getBoolean("user", null, "blockReviewDbGroupUpdates", false);
}
private static MetaDataUpdateFactory getMetaDataUpdateFactory(
@@ -204,9 +205,11 @@
public InternalGroup createGroup(
ReviewDb db, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
throws OrmException, IOException, ConfigInvalidException {
- InternalGroup createdGroupInReviewDb = createGroupInReviewDb(db, groupCreation, groupUpdate);
+ // TODO(ekempin): Don't read groups from ReviewDb if reading groups from NoteDb is configured
+ InternalGroup createdGroupInReviewDb =
+ createGroupInReviewDb(ReviewDbUtil.unwrapDb(db), groupCreation, groupUpdate);
- if (!writeGroupsToNoteDb) {
+ if (!groupsMigration.writeToNoteDb()) {
updateCachesOnGroupCreation(createdGroupInReviewDb);
return createdGroupInReviewDb;
}
@@ -240,10 +243,12 @@
public UpdateResult updateGroupInDb(
ReviewDb db, AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
- AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid);
- UpdateResult reviewDbUpdateResult = updateGroupInReviewDb(db, group, groupUpdate);
+ // TODO(ekempin): Don't read groups from ReviewDb if reading groups from NoteDb is configured
+ AccountGroup group = getExistingGroupFromReviewDb(ReviewDbUtil.unwrapDb(db), groupUuid);
+ UpdateResult reviewDbUpdateResult =
+ updateGroupInReviewDb(ReviewDbUtil.unwrapDb(db), group, groupUpdate);
- if (!writeGroupsToNoteDb) {
+ if (!groupsMigration.writeToNoteDb()) {
return reviewDbUpdateResult;
}
@@ -255,6 +260,7 @@
private InternalGroup createGroupInReviewDb(
ReviewDb db, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
throws OrmException {
+ checkIfReviewDbUpdatesAreBlocked();
AccountGroupName gn = new AccountGroupName(groupCreation.getNameKey(), groupCreation.getId());
// first insert the group name to validate that the group name hasn't
@@ -264,7 +270,10 @@
AccountGroup group = createAccountGroup(groupCreation);
UpdateResult updateResult = updateGroupInReviewDb(db, group, groupUpdate);
return InternalGroup.create(
- group, updateResult.getModifiedMembers(), updateResult.getModifiedSubgroups());
+ group,
+ updateResult.getModifiedMembers(),
+ updateResult.getModifiedSubgroups(),
+ updateResult.getRefState());
}
public static AccountGroup createAccountGroup(
@@ -291,6 +300,8 @@
private UpdateResult updateGroupInReviewDb(
ReviewDb db, AccountGroup group, InternalGroupUpdate groupUpdate) throws OrmException {
+ checkIfReviewDbUpdatesAreBlocked();
+
AccountGroup.NameKey originalName = group.getNameKey();
applyUpdate(group, groupUpdate);
AccountGroup.NameKey updatedName = group.getNameKey();
@@ -504,7 +515,8 @@
.setGroupId(updatedGroup.getId())
.setGroupName(updatedGroup.getNameKey())
.setModifiedMembers(modifiedMembers)
- .setModifiedSubgroups(modifiedSubgroups);
+ .setModifiedSubgroups(modifiedSubgroups)
+ .setRefState(updatedGroup.getRefState());
if (!Objects.equals(originalGroup.getNameKey(), updatedGroup.getNameKey())) {
resultBuilder.setPreviousGroupName(originalGroup.getNameKey());
}
@@ -574,9 +586,7 @@
}
}
- try (RevWalk revWalk = new RevWalk(allUsersRepo)) {
- RefUpdateUtil.executeChecked(batchRefUpdate, revWalk);
- }
+ RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
gitRefUpdated.fire(
allUsersName, batchRefUpdate, currentUser != null ? currentUser.getAccount() : null);
}
@@ -616,6 +626,12 @@
}
}
+ private void checkIfReviewDbUpdatesAreBlocked() throws OrmException {
+ if (reviewDbUpdatesAreBlocked) {
+ throw new OrmException("Updates to groups in ReviewDb are blocked");
+ }
+ }
+
@FunctionalInterface
private interface MetaDataUpdateFactory {
MetaDataUpdate create(
@@ -637,6 +653,9 @@
abstract ImmutableSet<AccountGroup.UUID> getModifiedSubgroups();
+ @Nullable
+ public abstract ObjectId getRefState();
+
static Builder builder() {
return new AutoValue_GroupsUpdate_UpdateResult.Builder();
}
@@ -655,6 +674,8 @@
abstract Builder setModifiedSubgroups(Set<AccountGroup.UUID> modifiedSubgroups);
+ public abstract Builder setRefState(ObjectId refState);
+
abstract UpdateResult build();
}
}
diff --git a/java/com/google/gerrit/server/group/db/testing/BUILD b/java/com/google/gerrit/server/group/db/testing/BUILD
new file mode 100644
index 0000000..1a5b598
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/testing/BUILD
@@ -0,0 +1,15 @@
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+ name = "testing",
+ testonly = 1,
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//lib:guava",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
new file mode 100644
index 0000000..a71f417
--- /dev/null
+++ b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2017 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.group.db.testing;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.group.db.GroupNameNotes.getGroupReference;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Streams;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.CommitUtil;
+import java.io.IOException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Test utilities for low-level NoteDb groups. */
+public class GroupTestUtil {
+ public static ImmutableMap<String, String> readNameToUuidMap(Repository repo) throws Exception {
+ ImmutableMap.Builder<String, String> result = ImmutableMap.builder();
+ try (RevWalk rw = new RevWalk(repo)) {
+ Ref ref = repo.exactRef(RefNames.REFS_GROUPNAMES);
+ if (ref != null) {
+ NoteMap noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(ref.getObjectId()));
+ for (Note note : noteMap) {
+ GroupReference gr = getGroupReference(rw.getObjectReader(), note.getData());
+ result.put(gr.getName(), gr.getUUID().get());
+ }
+ }
+ }
+ return result.build();
+ }
+
+ // TODO(dborowitz): Move somewhere even more common.
+ public static ImmutableList<CommitInfo> log(Repository repo, String refName) throws Exception {
+ try (RevWalk rw = new RevWalk(repo)) {
+ Ref ref = repo.exactRef(refName);
+ if (ref != null) {
+ rw.sort(RevSort.REVERSE);
+ rw.markStart(rw.parseCommit(ref.getObjectId()));
+ return Streams.stream(rw)
+ .map(
+ c -> {
+ try {
+ return CommitUtil.toCommitInfo(c);
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "unexpected state when converting commit " + c.getName(), e);
+ }
+ })
+ .collect(toImmutableList());
+ }
+ }
+ return ImmutableList.of();
+ }
+
+ private GroupTestUtil() {}
+}
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index b37ed61..2abe876 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
+import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
@@ -30,12 +31,27 @@
import com.google.gerrit.server.query.change.SingleGroupUser;
import java.io.IOException;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.errors.ConfigInvalidException;
public final class IndexUtils {
public static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING =
ImmutableMap.of("_", " ", ".", " ");
+ public static final Function<Exception, IOException> MAPPER =
+ new Function<Exception, IOException>() {
+ @Override
+ public IOException apply(Exception in) {
+ if (in instanceof IOException) {
+ return (IOException) in;
+ } else if (in instanceof ExecutionException && in.getCause() instanceof IOException) {
+ return (IOException) in.getCause();
+ } else {
+ return new IOException(in);
+ }
+ }
+ };
+
public static void setReady(SitePaths sitePaths, String name, int version, boolean ready)
throws IOException {
try {
@@ -57,10 +73,13 @@
}
public static Set<String> accountFields(QueryOptions opts) {
- Set<String> fs = opts.fields();
- return fs.contains(AccountField.ID.getName())
- ? fs
- : Sets.union(fs, ImmutableSet.of(AccountField.ID.getName()));
+ return accountFields(opts.fields());
+ }
+
+ public static Set<String> accountFields(Set<String> fields) {
+ return fields.contains(AccountField.ID.getName())
+ ? fields
+ : Sets.union(fields, ImmutableSet.of(AccountField.ID.getName()));
}
public static Set<String> changeFields(QueryOptions opts) {
diff --git a/java/com/google/gerrit/server/index/RefState.java b/java/com/google/gerrit/server/index/RefState.java
index b55afb6..6b893f0 100644
--- a/java/com/google/gerrit/server/index/RefState.java
+++ b/java/com/google/gerrit/server/index/RefState.java
@@ -19,9 +19,13 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Project;
import java.io.IOException;
+import java.util.List;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -29,6 +33,20 @@
@AutoValue
public abstract class RefState {
+ public static SetMultimap<Project.NameKey, RefState> parseStates(Iterable<byte[]> states) {
+ RefState.check(states != null, null);
+ SetMultimap<Project.NameKey, RefState> result =
+ MultimapBuilder.hashKeys().hashSetValues().build();
+ for (byte[] b : states) {
+ RefState.check(b != null, null);
+ String s = new String(b, UTF_8);
+ List<String> parts = Splitter.on(':').splitToList(s);
+ RefState.check(parts.size() == 3 && !parts.get(0).isEmpty() && !parts.get(1).isEmpty(), s);
+ result.put(new Project.NameKey(parts.get(0)), RefState.create(parts.get(1), parts.get(2)));
+ }
+ return result;
+ }
+
public static RefState create(String ref, String sha) {
return new AutoValue_RefState(ref, ObjectId.fromString(sha));
}
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index 6ec1260..b0527e1 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -14,7 +14,11 @@
package com.google.gerrit.server.index.account;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+
import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.events.AccountIndexedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -22,11 +26,17 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.IndexUtils;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import org.eclipse.jgit.lib.Config;
public class AccountIndexerImpl implements AccountIndexer {
public interface Factory {
@@ -37,6 +47,9 @@
private final AccountCache byIdCache;
private final DynamicSet<AccountIndexedListener> indexedListener;
+ private final StalenessChecker stalenessChecker;
+ private final ListeningExecutorService batchExecutor;
+ private final boolean autoReindexIfStale;
private final AccountIndexCollection indexes;
private final AccountIndex index;
@@ -44,9 +57,15 @@
AccountIndexerImpl(
AccountCache byIdCache,
DynamicSet<AccountIndexedListener> indexedListener,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+ @GerritServerConfig Config config,
@Assisted AccountIndexCollection indexes) {
this.byIdCache = byIdCache;
this.indexedListener = indexedListener;
+ this.stalenessChecker = stalenessChecker;
+ this.batchExecutor = batchExecutor;
+ this.autoReindexIfStale = autoReindexIfStale(config);
this.indexes = indexes;
this.index = null;
}
@@ -55,9 +74,15 @@
AccountIndexerImpl(
AccountCache byIdCache,
DynamicSet<AccountIndexedListener> indexedListener,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
+ @GerritServerConfig Config config,
@Assisted AccountIndex index) {
this.byIdCache = byIdCache;
this.indexedListener = indexedListener;
+ this.stalenessChecker = stalenessChecker;
+ this.batchExecutor = batchExecutor;
+ this.autoReindexIfStale = autoReindexIfStale(config);
this.indexes = null;
this.index = index;
}
@@ -73,6 +98,44 @@
}
}
fireAccountIndexedEvent(id.get());
+ autoReindexIfStale(id);
+ }
+
+ private static boolean autoReindexIfStale(Config cfg) {
+ return cfg.getBoolean("index", null, "autoReindexIfStale", false);
+ }
+
+ private void autoReindexIfStale(Account.Id id) {
+ if (autoReindexIfStale) {
+ // Don't retry indefinitely; if this fails the account will be stale.
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError = reindexIfStale(id);
+ }
+ }
+
+ /**
+ * Asynchronously check if a account is stale, and reindex if it is.
+ *
+ * <p>Always run on the batch executor, even if this indexer instance is configured to use a
+ * different executor.
+ *
+ * @param id the ID of the account.
+ * @return future for reindexing the account; returns true if the account was stale.
+ */
+ @SuppressWarnings("deprecation")
+ public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
+ Account.Id id) {
+ Callable<Boolean> task =
+ () -> {
+ if (stalenessChecker.isStale(id)) {
+ index(id);
+ return true;
+ }
+ return false;
+ };
+
+ return Futures.makeChecked(
+ Futures.nonCancellationPropagating(batchExecutor.submit(task)), IndexUtils.MAPPER);
}
private void fireAccountIndexedEvent(int id) {
diff --git a/java/com/google/gerrit/server/index/account/StalenessChecker.java b/java/com/google/gerrit/server/index/account/StalenessChecker.java
new file mode 100644
index 0000000..6403d3d
--- /dev/null
+++ b/java/com/google/gerrit/server/index/account/StalenessChecker.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2017 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.index.account;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.IndexUtils;
+import com.google.gerrit.server.index.RefState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Checks if documents in the account index are stale.
+ *
+ * <p>An index document is considered stale if the stored ref state differs from the SHA1 of the
+ * user branch or if the stored external ID states don't match with the external IDs of the account
+ * from the refs/meta/external-ids branch.
+ */
+@Singleton
+public class StalenessChecker {
+ public static final ImmutableSet<String> FIELDS =
+ ImmutableSet.of(
+ AccountField.ID.getName(),
+ AccountField.REF_STATE.getName(),
+ AccountField.EXTERNAL_ID_STATE.getName());
+
+ private final AccountIndexCollection indexes;
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsersName;
+ private final ExternalIds externalIds;
+ private final IndexConfig indexConfig;
+
+ @Inject
+ StalenessChecker(
+ AccountIndexCollection indexes,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ ExternalIds externalIds,
+ IndexConfig indexConfig) {
+ this.indexes = indexes;
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ this.externalIds = externalIds;
+ this.indexConfig = indexConfig;
+ }
+
+ public boolean isStale(Account.Id id) throws IOException {
+ AccountIndex i = indexes.getSearchIndex();
+ if (i == null) {
+ // No index; caller couldn't do anything if it is stale.
+ return false;
+ }
+ if (!i.getSchema().hasField(AccountField.REF_STATE)
+ || !i.getSchema().hasField(AccountField.EXTERNAL_ID_STATE)) {
+ // Index version not new enough for this check.
+ return false;
+ }
+
+ Optional<FieldBundle> result =
+ i.getRaw(id, QueryOptions.create(indexConfig, 0, 1, IndexUtils.accountFields(FIELDS)));
+ if (!result.isPresent()) {
+ // The document is missing in the index.
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ Ref ref = repo.exactRef(RefNames.refsUsers(id));
+
+ // Stale if the account actually exists.
+ return ref != null;
+ }
+ }
+
+ for (Map.Entry<Project.NameKey, RefState> e :
+ RefState.parseStates(result.get().getValue(AccountField.REF_STATE)).entries()) {
+ try (Repository repo = repoManager.openRepository(e.getKey())) {
+ if (!e.getValue().match(repo)) {
+ // Ref was modified since the account was indexed.
+ return true;
+ }
+ }
+ }
+
+ Set<ExternalId> extIds = externalIds.byAccount(id);
+ ListMultimap<ObjectId, ObjectId> extIdStates =
+ parseExternalIdStates(result.get().getValue(AccountField.EXTERNAL_ID_STATE));
+ if (extIdStates.size() != extIds.size()) {
+ // External IDs of the account were modified since the account was indexed.
+ return true;
+ }
+ for (ExternalId extId : extIds) {
+ if (!extIdStates.containsEntry(extId.key().sha1(), extId.blobId())) {
+ // External IDs of the account were modified since the account was indexed.
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static ListMultimap<ObjectId, ObjectId> parseExternalIdStates(
+ Iterable<byte[]> extIdStates) {
+ ListMultimap<ObjectId, ObjectId> result = MultimapBuilder.hashKeys().arrayListValues().build();
+
+ if (extIdStates == null) {
+ return result;
+ }
+
+ for (byte[] b : extIdStates) {
+ checkNotNull(b, "invalid external ID state");
+ String s = new String(b, UTF_8);
+ List<String> parts = Splitter.on(':').splitToList(s);
+ checkState(parts.size() == 2, "invalid external ID state: %s", s);
+ result.put(ObjectId.fromString(parts.get(0)), ObjectId.fromString(parts.get(1)));
+ }
+ return result;
+ }
+}
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index f62b662..d205e91 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -17,7 +17,6 @@
import static com.google.gerrit.server.extensions.events.EventUtil.logEventListenerError;
import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
-import com.google.common.base.Function;
import com.google.common.util.concurrent.Atomics;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -31,6 +30,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.query.change.ChangeData;
@@ -50,7 +50,6 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jgit.lib.Config;
@@ -79,23 +78,9 @@
// ExecutionException, so we can reuse the same mapper as for a single
// future. Assume the actual contents of the exception are not useful to
// callers. All exceptions are already logged by IndexTask.
- return Futures.makeChecked(Futures.allAsList(futures), MAPPER);
+ return Futures.makeChecked(Futures.allAsList(futures), IndexUtils.MAPPER);
}
- private static final Function<Exception, IOException> MAPPER =
- new Function<Exception, IOException>() {
- @Override
- public IOException apply(Exception in) {
- if (in instanceof IOException) {
- return (IOException) in;
- } else if (in instanceof ExecutionException && in.getCause() instanceof IOException) {
- return (IOException) in.getCause();
- } else {
- return new IOException(in);
- }
- }
- };
-
private final ChangeIndexCollection indexes;
private final ChangeIndex index;
private final SchemaFactory<ReviewDb> schemaFactory;
@@ -335,7 +320,8 @@
@SuppressWarnings("deprecation")
private static <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
Callable<T> task, ListeningExecutorService executor) {
- return Futures.makeChecked(Futures.nonCancellationPropagating(executor.submit(task)), MAPPER);
+ return Futures.makeChecked(
+ Futures.nonCancellationPropagating(executor.submit(task)), IndexUtils.MAPPER);
}
private abstract class AbstractIndexTask<T> implements Callable<T> {
diff --git a/java/com/google/gerrit/server/index/change/StalenessChecker.java b/java/com/google/gerrit/server/index/change/StalenessChecker.java
index e804702..e7790df 100644
--- a/java/com/google/gerrit/server/index/change/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -158,21 +158,7 @@
}
private SetMultimap<Project.NameKey, RefState> parseStates(ChangeData cd) {
- return parseStates(cd.getRefStates());
- }
-
- public static SetMultimap<Project.NameKey, RefState> parseStates(Iterable<byte[]> states) {
- RefState.check(states != null, null);
- SetMultimap<Project.NameKey, RefState> result =
- MultimapBuilder.hashKeys().hashSetValues().build();
- for (byte[] b : states) {
- RefState.check(b != null, null);
- String s = new String(b, UTF_8);
- List<String> parts = Splitter.on(':').splitToList(s);
- RefState.check(parts.size() == 3 && !parts.get(0).isEmpty() && !parts.get(1).isEmpty(), s);
- result.put(new Project.NameKey(parts.get(0)), RefState.create(parts.get(1), parts.get(2)));
- }
- return result;
+ return RefState.parseStates(cd.getRefStates());
}
private ListMultimap<Project.NameKey, RefStatePattern> parsePatterns(ChangeData cd) {
diff --git a/java/com/google/gerrit/server/index/group/GroupField.java b/java/com/google/gerrit/server/index/group/GroupField.java
index 078433a..29e3867 100644
--- a/java/com/google/gerrit/server/index/group/GroupField.java
+++ b/java/com/google/gerrit/server/index/group/GroupField.java
@@ -19,14 +19,18 @@
import static com.google.gerrit.index.FieldDef.fullText;
import static com.google.gerrit.index.FieldDef.integer;
import static com.google.gerrit.index.FieldDef.prefix;
+import static com.google.gerrit.index.FieldDef.storedOnly;
import static com.google.gerrit.index.FieldDef.timestamp;
+import com.google.common.base.MoreObjects;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.SchemaUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.group.InternalGroup;
import java.sql.Timestamp;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
/** Secondary index schemas for groups. */
public class GroupField {
@@ -72,4 +76,14 @@
.buildRepeatable(
g ->
g.getSubgroups().stream().map(AccountGroup.UUID::get).collect(toImmutableList()));
+
+ /** ObjectId of HEAD:refs/groups/<UUID>. */
+ public static final FieldDef<InternalGroup, byte[]> REF_STATE =
+ storedOnly("ref_state")
+ .build(
+ g -> {
+ byte[] a = new byte[Constants.OBJECT_ID_STRING_LENGTH];
+ MoreObjects.firstNonNull(g.getRefState(), ObjectId.zeroId()).copyTo(a, 0);
+ return a;
+ });
}
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index 69b29bc..58caeb2 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -14,20 +14,30 @@
package com.google.gerrit.server.index.group;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+
import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.events.GroupIndexedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.index.Index;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.IndexUtils;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import org.eclipse.jgit.lib.Config;
public class GroupIndexerImpl implements GroupIndexer {
public interface Factory {
@@ -39,16 +49,25 @@
private final GroupCache groupCache;
private final DynamicSet<GroupIndexedListener> indexedListener;
private final GroupIndexCollection indexes;
+ private final StalenessChecker stalenessChecker;
+ private final boolean autoReindexIfStale;
private final GroupIndex index;
+ private final ListeningExecutorService batchExecutor;
@AssistedInject
GroupIndexerImpl(
GroupCache groupCache,
DynamicSet<GroupIndexedListener> indexedListener,
+ StalenessChecker stalenessChecker,
+ @GerritServerConfig Config config,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
@Assisted GroupIndexCollection indexes) {
this.groupCache = groupCache;
this.indexedListener = indexedListener;
this.indexes = indexes;
+ this.stalenessChecker = stalenessChecker;
+ this.autoReindexIfStale = autoReindexIfStale(config);
+ this.batchExecutor = batchExecutor;
this.index = null;
}
@@ -56,10 +75,16 @@
GroupIndexerImpl(
GroupCache groupCache,
DynamicSet<GroupIndexedListener> indexedListener,
+ StalenessChecker stalenessChecker,
+ @GerritServerConfig Config config,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
@Assisted GroupIndex index) {
this.groupCache = groupCache;
this.indexedListener = indexedListener;
this.indexes = null;
+ this.stalenessChecker = stalenessChecker;
+ this.autoReindexIfStale = autoReindexIfStale(config);
+ this.batchExecutor = batchExecutor;
this.index = index;
}
@@ -74,6 +99,44 @@
}
}
fireGroupIndexedEvent(uuid.get());
+ autoReindexIfStale(uuid);
+ }
+
+ private static boolean autoReindexIfStale(Config cfg) {
+ return cfg.getBoolean("index", null, "autoReindexIfStale", true);
+ }
+
+ private void autoReindexIfStale(AccountGroup.UUID uuid) {
+ if (autoReindexIfStale) {
+ // Don't retry indefinitely; if this fails the group will be stale.
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError = reindexIfStale(uuid);
+ }
+ }
+
+ /**
+ * Asynchronously check if a group is stale, and reindex if it is.
+ *
+ * <p>Always run on the batch executor, even if this indexer instance is configured to use a
+ * different executor.
+ *
+ * @param uuid the unique identifier of the group.
+ * @return future for reindexing the group; returns true if the group was stale.
+ */
+ @SuppressWarnings("deprecation")
+ public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
+ AccountGroup.UUID uuid) {
+ Callable<Boolean> task =
+ () -> {
+ if (stalenessChecker.isStale(uuid)) {
+ index(uuid);
+ return true;
+ }
+ return false;
+ };
+
+ return Futures.makeChecked(
+ Futures.nonCancellationPropagating(batchExecutor.submit(task)), IndexUtils.MAPPER);
}
private void fireGroupIndexedEvent(String uuid) {
diff --git a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
index b280b25..912524f 100644
--- a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -34,8 +34,11 @@
@Deprecated static final Schema<InternalGroup> V3 = schema(V2, GroupField.CREATED_ON);
+ @Deprecated
static final Schema<InternalGroup> V4 = schema(V3, GroupField.MEMBER, GroupField.SUBGROUP);
+ static final Schema<InternalGroup> V5 = schema(V4, GroupField.REF_STATE);
+
public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();
private GroupSchemaDefinitions() {
diff --git a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
index 255df32..79f25c0 100644
--- a/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
+++ b/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.index.group;
import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.IndexedQuery;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.query.DataSource;
@@ -22,10 +23,22 @@
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.group.InternalGroup;
+import java.util.HashSet;
+import java.util.Set;
public class IndexedGroupQuery extends IndexedQuery<AccountGroup.UUID, InternalGroup>
implements DataSource<InternalGroup> {
+ public static QueryOptions createOptions(
+ IndexConfig config, int start, int limit, Set<String> fields) {
+ // Always include GroupField.UUID since it is needed to load the group from NoteDb.
+ if (!fields.contains(GroupField.UUID.getName())) {
+ fields = new HashSet<>(fields);
+ fields.add(GroupField.UUID.getName());
+ }
+ return QueryOptions.create(config, start, limit, fields);
+ }
+
public IndexedGroupQuery(
Index<AccountGroup.UUID, InternalGroup> index,
Predicate<InternalGroup> pred,
diff --git a/java/com/google/gerrit/server/index/group/StalenessChecker.java b/java/com/google/gerrit/server/index/group/StalenessChecker.java
new file mode 100644
index 0000000..418bb35
--- /dev/null
+++ b/java/com/google/gerrit/server/index/group/StalenessChecker.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2017 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.index.group;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.GroupsMigration;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Checks if documents in the group index are stale.
+ *
+ * <p>An index document is considered stale if the stored SHA1 differs from the HEAD SHA1 of the
+ * groups branch.
+ *
+ * <p>Note: This only applies to NoteDb.
+ */
+@Singleton
+public class StalenessChecker {
+ public static final ImmutableSet<String> FIELDS =
+ ImmutableSet.of(GroupField.UUID.getName(), GroupField.REF_STATE.getName());
+
+ private final GroupIndexCollection indexes;
+ private final GitRepositoryManager repoManager;
+ private final IndexConfig indexConfig;
+ private final AllUsersName allUsers;
+ private final GroupsMigration groupsMigration;
+
+ @Inject
+ StalenessChecker(
+ GroupIndexCollection indexes,
+ GitRepositoryManager repoManager,
+ IndexConfig indexConfig,
+ AllUsersName allUsers,
+ GroupsMigration groupsMigration) {
+ this.indexes = indexes;
+ this.repoManager = repoManager;
+ this.indexConfig = indexConfig;
+ this.allUsers = allUsers;
+ this.groupsMigration = groupsMigration;
+ }
+
+ public boolean isStale(AccountGroup.UUID uuid) throws IOException {
+ if (!groupsMigration.readFromNoteDb()) {
+ return false; // This class only treats staleness for groups in NoteDb.
+ }
+
+ GroupIndex i = indexes.getSearchIndex();
+ if (i == null) {
+ return false; // No index; caller couldn't do anything if it is stale.
+ }
+ if (!i.getSchema().hasField(GroupField.REF_STATE)) {
+ return false; // Index version not new enough for this check.
+ }
+
+ Optional<FieldBundle> result =
+ i.getRaw(uuid, IndexedGroupQuery.createOptions(indexConfig, 0, 1, FIELDS));
+ if (!result.isPresent()) {
+ // The document is missing in the index.
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ Ref ref = repo.exactRef(RefNames.refsGroups(uuid));
+
+ // Stale if the group actually exists.
+ return ref != null;
+ }
+ }
+
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ Ref ref = repo.exactRef(RefNames.refsGroups(uuid));
+ ObjectId head = ref == null ? ObjectId.zeroId() : ref.getObjectId();
+ return !head.equals(ObjectId.fromString(result.get().getValue(GroupField.REF_STATE), 0));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/ChangeBundle.java b/java/com/google/gerrit/server/notedb/ChangeBundle.java
index a9663c7..221252c 100644
--- a/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -17,8 +17,8 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.common.TimeUtil.roundToSecond;
+import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
@@ -71,7 +71,6 @@
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
-import java.util.TreeSet;
/**
* A bundle of all entities rooted at a single {@link Change} entity.
@@ -212,23 +211,6 @@
.compare(a.get(), b.get());
}
- private static void checkColumns(Class<?> clazz, Integer... expected) {
- Set<Integer> ids = new TreeSet<>();
- for (Field f : clazz.getDeclaredFields()) {
- Column col = f.getAnnotation(Column.class);
- if (col != null) {
- ids.add(col.id());
- }
- }
- Set<Integer> expectedIds = Sets.newTreeSet(Arrays.asList(expected));
- checkState(
- ids.equals(expectedIds),
- "Unexpected column set for %s: %s != %s",
- clazz.getSimpleName(),
- ids,
- expectedIds);
- }
-
static {
// Initialization-time checks that the column set hasn't changed since the
// last time this file was updated.
diff --git a/java/com/google/gerrit/server/notedb/GroupsMigration.java b/java/com/google/gerrit/server/notedb/GroupsMigration.java
new file mode 100644
index 0000000..1ca67a4
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/GroupsMigration.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.READ;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Set;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class GroupsMigration {
+ public static class Module extends AbstractModule {
+ @Override
+ public void configure() {
+ bind(GroupsMigration.class);
+ }
+ }
+
+ private final boolean writeToNoteDb;
+ private final boolean readFromNoteDb;
+
+ @Inject
+ public GroupsMigration(@GerritServerConfig Config cfg) {
+ // TODO(aliceks): Remove these flags when all other necessary TODOs for writing groups to
+ // NoteDb have been addressed.
+ // Don't flip these flags in a production setting! We only added them to spread the
+ // implementation of groups in NoteDb among several changes which are gradually merged.
+ this(
+ cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, false),
+ cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, false));
+ }
+
+ public GroupsMigration(boolean writeToNoteDb, boolean readFromNoteDb) {
+ this.writeToNoteDb = writeToNoteDb;
+ this.readFromNoteDb = readFromNoteDb;
+ }
+
+ public boolean writeToNoteDb() {
+ return writeToNoteDb;
+ }
+
+ public boolean readFromNoteDb() {
+ return readFromNoteDb;
+ }
+
+ public void setConfigValuesIfNotSetYet(Config cfg) {
+ Set<String> subsections = cfg.getSubsections(SECTION_NOTE_DB);
+ if (!subsections.contains(GROUPS.key())) {
+ cfg.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, writeToNoteDb());
+ cfg.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, readFromNoteDb());
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbTable.java b/java/com/google/gerrit/server/notedb/NoteDbTable.java
index be24e28..e299fdf 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbTable.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbTable.java
@@ -16,6 +16,7 @@
public enum NoteDbTable {
ACCOUNTS,
+ GROUPS,
CHANGES;
public String key() {
diff --git a/java/com/google/gerrit/server/notedb/NotesMigration.java b/java/com/google/gerrit/server/notedb/NotesMigration.java
index e560ec8..bab756e 100644
--- a/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -54,12 +54,12 @@
*/
public abstract class NotesMigration {
public static final String SECTION_NOTE_DB = "noteDb";
+ public static final String READ = "read";
+ public static final String WRITE = "write";
private static final String DISABLE_REVIEW_DB = "disableReviewDb";
private static final String PRIMARY_STORAGE = "primaryStorage";
- private static final String READ = "read";
private static final String SEQUENCE = "sequence";
- private static final String WRITE = "write";
public static class Module extends AbstractModule {
@Override
diff --git a/java/com/google/gerrit/server/patch/PatchListLoader.java b/java/com/google/gerrit/server/patch/PatchListLoader.java
index 8fce6d3..bf6e345 100644
--- a/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -510,7 +510,7 @@
byte[] aContent = aText.getContent();
byte[] bContent = bText.getContent();
long size = bContent.length;
- long sizeDelta = bContent.length - aContent.length;
+ long sizeDelta = size - aContent.length;
RawText aRawText = new RawText(aContent);
RawText bRawText = new RawText(bContent);
EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
diff --git a/java/com/google/gerrit/server/plugins/PluginRestApiModule.java b/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
index 5f97134..8e162ba 100644
--- a/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
+++ b/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
@@ -16,11 +16,8 @@
import static com.google.gerrit.server.plugins.PluginResource.PLUGIN_KIND;
-import com.google.gerrit.extensions.api.plugins.Plugins;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.server.api.plugins.PluginApiImpl;
-import com.google.gerrit.server.api.plugins.PluginsImpl;
public class PluginRestApiModule extends RestApiModule {
@Override
@@ -33,7 +30,5 @@
post(PLUGIN_KIND, "disable").to(DisablePlugin.class);
post(PLUGIN_KIND, "enable").to(EnablePlugin.class);
post(PLUGIN_KIND, "reload").to(ReloadPlugin.class);
- bind(Plugins.class).to(PluginsImpl.class);
- factory(PluginApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/project/GetCommit.java b/java/com/google/gerrit/server/project/GetCommit.java
index 2afeb07..d8fc5b6 100644
--- a/java/com/google/gerrit/server/project/GetCommit.java
+++ b/java/com/google/gerrit/server/project/GetCommit.java
@@ -18,12 +18,13 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.git.CommitUtil;
import com.google.inject.Singleton;
+import java.io.IOException;
@Singleton
public class GetCommit implements RestReadView<CommitResource> {
@Override
- public CommitInfo apply(CommitResource rsrc) {
+ public CommitInfo apply(CommitResource rsrc) throws IOException {
return CommitUtil.toCommitInfo(rsrc.getCommit());
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 3fe838d..4b2161b 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -134,6 +134,7 @@
try {
return checkedGet(projectName);
} catch (IOException e) {
+ log.warn("Cannot read project " + projectName, e);
return null;
}
}
@@ -232,6 +233,7 @@
try {
src = list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx));
} catch (ExecutionException e) {
+ log.warn("Cannot look up projects for prefix " + pfx, e);
return Collections.emptyList();
}
return new Iterable<Project.NameKey>() {
diff --git a/java/com/google/gerrit/server/schema/AllUsersCreator.java b/java/com/google/gerrit/server/schema/AllUsersCreator.java
index 3049e86..42c86c1 100644
--- a/java/com/google/gerrit/server/schema/AllUsersCreator.java
+++ b/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -112,6 +112,12 @@
grant(config, defaults, Permission.CREATE, admin);
}
+ // Grant read permissions on the group branches to all users.
+ // This allows group owners to see the group refs. VisibleRefFilter ensures that read
+ // permissions for non-group-owners are ignored.
+ AccessSection groups = config.getAccessSection(RefNames.REFS_GROUPS + "*", true);
+ grant(config, groups, Permission.READ, false, true, registered);
+
config.commit(md);
}
}
diff --git a/java/com/google/gerrit/server/schema/DataSourceProvider.java b/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 67f6894..98acc64 100644
--- a/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -138,6 +138,9 @@
MILLISECONDS.convert(30, SECONDS),
MILLISECONDS));
ds.setInitialSize(ds.getMinIdle());
+ long evictIdleTimeMs = 1000L * 60;
+ ds.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
+ ds.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
ds.setValidationQuery(dst.getValidationQuery());
ds.setValidationQueryTimeout(5);
exportPoolMetrics(ds);
diff --git a/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java b/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
index d73a5f4..c80e06b 100644
--- a/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
+++ b/java/com/google/gerrit/server/schema/NotesMigrationSchemaFactory.java
@@ -15,7 +15,9 @@
package com.google.gerrit.server.schema;
import com.google.gerrit.reviewdb.server.DisallowReadFromChangesReviewDbWrapper;
+import com.google.gerrit.reviewdb.server.DisallowReadFromGroupsReviewDbWrapper;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -26,48 +28,58 @@
public class NotesMigrationSchemaFactory implements SchemaFactory<ReviewDb> {
private final SchemaFactory<ReviewDb> delegate;
private final NotesMigration migration;
+ private final GroupsMigration groupsMigration;
@Inject
NotesMigrationSchemaFactory(
- @ReviewDbFactory SchemaFactory<ReviewDb> delegate, NotesMigration migration) {
+ @ReviewDbFactory SchemaFactory<ReviewDb> delegate,
+ NotesMigration migration,
+ GroupsMigration groupsMigration) {
this.delegate = delegate;
this.migration = migration;
+ this.groupsMigration = groupsMigration;
}
@Override
public ReviewDb open() throws OrmException {
ReviewDb db = delegate.open();
- if (!migration.readChanges()) {
- return db;
+ if (migration.readChanges()) {
+ // There are two levels at which this class disables access to Changes and related tables,
+ // corresponding to two phases of the NoteDb migration:
+ //
+ // 1. When changes are read from NoteDb but some changes might still have their primary
+ // storage in ReviewDb, it is generally programmer error to read changes from ReviewDb.
+ // However, since ReviewDb is still the primary storage for most or all changes, we still
+ // need to support writing to ReviewDb. This behavior is accomplished by wrapping in a
+ // DisallowReadFromChangesReviewDbWrapper.
+ //
+ // Some codepaths might need to be able to read from ReviewDb if they really need to,
+ // because they need to operate on the underlying source of truth, for example when
+ // reading a change to determine its primary storage. To support this,
+ // ReviewDbUtil#unwrapDb can detect and unwrap databases of this type.
+ //
+ // 2. After all changes have their primary storage in NoteDb, we can completely shut off
+ // access to the change tables. At this point in the migration, we are by definition not
+ // using the ReviewDb tables at all; we could even delete the tables at this point, and
+ // Gerrit would continue to function.
+ //
+ // This is accomplished by setting the delegate ReviewDb *underneath*
+ // DisallowReadFromChanges to be a complete no-op, with NoChangesReviewDbWrapper. With
+ // this wrapper, all read operations return no results, and write operations silently do
+ // nothing. This wrapper is not a public class and nobody should ever attempt to unwrap
+ // it.
+
+ if (migration.disableChangeReviewDb()) {
+ db = new NoChangesReviewDbWrapper(db);
+ }
+ db = new DisallowReadFromChangesReviewDbWrapper(db);
}
- // There are two levels at which this class disables access to Changes and related tables,
- // corresponding to two phases of the NoteDb migration:
- //
- // 1. When changes are read from NoteDb but some changes might still have their primary storage
- // in ReviewDb, it is generally programmer error to read changes from ReviewDb. However,
- // since ReviewDb is still the primary storage for most or all changes, we still need to
- // support writing to ReviewDb. This behavior is accomplished by wrapping in a
- // DisallowReadFromChangesReviewDbWrapper.
- //
- // Some codepaths might need to be able to read from ReviewDb if they really need to, because
- // they need to operate on the underlying source of truth, for example when reading a change
- // to determine its primary storage. To support this, ReviewDbUtil#unwrapDb can detect and
- // unwrap databases of this type.
- //
- // 2. After all changes have their primary storage in NoteDb, we can completely shut off access
- // to the change tables. At this point in the migration, we are by definition not using the
- // ReviewDb tables at all; we could even delete the tables at this point, and Gerrit would
- // continue to function.
- //
- // This is accomplished by setting the delegate ReviewDb *underneath* DisallowReadFromChanges
- // to be a complete no-op, with NoChangesReviewDbWrapper. With this wrapper, all read
- // operations return no results, and write operations silently do nothing. This wrapper is
- // not a public class and nobody should ever attempt to unwrap it.
-
- if (migration.disableChangeReviewDb()) {
- db = new NoChangesReviewDbWrapper(db);
+ if (groupsMigration.readFromNoteDb()) {
+ // If reading groups from NoteDb is configured, groups should not be read from ReviewDb.
+ // Make sure that any attempt to read a group from ReviewDb anyway fails with an exception.
+ db = new DisallowReadFromGroupsReviewDbWrapper(db);
}
- return new DisallowReadFromChangesReviewDbWrapper(db);
+ return db;
}
}
diff --git a/java/com/google/gerrit/server/schema/SchemaCreator.java b/java/com/google/gerrit/server/schema/SchemaCreator.java
index eb05360..112fd55 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -45,6 +45,7 @@
import com.google.gerrit.server.group.db.InternalGroupUpdate;
import com.google.gerrit.server.index.group.GroupIndex;
import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gwtorm.jdbc.JdbcExecutor;
@@ -60,7 +61,6 @@
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
/** Creates the current database schema and populates initial code rows. */
public class SchemaCreator {
@@ -73,7 +73,7 @@
private final PersonIdent serverUser;
private final DataSourceType dataSourceType;
private final GroupIndexCollection indexCollection;
- private final boolean writeGroupsToNoteDb;
+ private final GroupsMigration groupsMigration;
private final Config config;
private final MetricMaker metricMaker;
@@ -90,6 +90,7 @@
@GerritPersonIdent PersonIdent au,
DataSourceType dst,
GroupIndexCollection ic,
+ GroupsMigration gm,
@GerritServerConfig Config config,
MetricMaker metricMaker,
NotesMigration migration,
@@ -103,6 +104,7 @@
au,
dst,
ic,
+ gm,
config,
metricMaker,
migration,
@@ -118,6 +120,7 @@
@GerritPersonIdent PersonIdent au,
DataSourceType dst,
GroupIndexCollection ic,
+ GroupsMigration gm,
Config config,
MetricMaker metricMaker,
NotesMigration migration,
@@ -130,11 +133,7 @@
serverUser = au;
dataSourceType = dst;
indexCollection = ic;
- // TODO(aliceks): Remove this flag when all other necessary TODOs for writing groups to NoteDb
- // have been addressed.
- // Don't flip this flag in a production setting! We only added it to spread the implementation
- // of groups in NoteDb among several changes which are gradually merged.
- writeGroupsToNoteDb = config.getBoolean("user", null, "writeGroupsToNoteDb", false);
+ groupsMigration = gm;
this.config = config;
this.allProjectsName = apName;
@@ -214,7 +213,7 @@
throws OrmException, ConfigInvalidException, IOException {
InternalGroup groupInReviewDb = createGroupInReviewDb(db, groupCreation, groupUpdate);
- if (!writeGroupsToNoteDb) {
+ if (!groupsMigration.writeToNoteDb()) {
index(groupInReviewDb);
return;
}
@@ -262,9 +261,7 @@
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(allUsersRepo, batchRefUpdate)) {
groupNameNotes.commit(metaDataUpdate);
}
- try (RevWalk revWalk = new RevWalk(allUsersRepo)) {
- RefUpdateUtil.executeChecked(batchRefUpdate, revWalk);
- }
+ RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
}
private MetaDataUpdate createMetaDataUpdate(
diff --git a/java/com/google/gerrit/server/schema/SchemaVersion.java b/java/com/google/gerrit/server/schema/SchemaVersion.java
index 2cb57a8..780b70a 100644
--- a/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
- public static final Class<Schema_163> C = Schema_163.class;
+ public static final Class<Schema_164> C = Schema_164.class;
public static int getBinaryVersion() {
return guessVersion(C);
diff --git a/java/com/google/gerrit/server/schema/Schema_164.java b/java/com/google/gerrit/server/schema/Schema_164.java
new file mode 100644
index 0000000..fca0ae8
--- /dev/null
+++ b/java/com/google/gerrit/server/schema/Schema_164.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2017 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.schema;
+
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.schema.AclUtil.grant;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.sql.SQLException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/** Grant read on group branches */
+public class Schema_164 extends SchemaVersion {
+ private static final String COMMIT_MSG = "Grant read permissions on group branches";
+
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsersName;
+ private final SystemGroupBackend systemGroupBackend;
+ private final PersonIdent serverUser;
+
+ @Inject
+ Schema_164(
+ Provider<Schema_163> prior,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ SystemGroupBackend systemGroupBackend,
+ @GerritPersonIdent PersonIdent serverUser) {
+ super(prior);
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ this.systemGroupBackend = systemGroupBackend;
+ this.serverUser = serverUser;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+ try (Repository git = repoManager.openRepository(allUsersName);
+ MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+ md.setMessage(COMMIT_MSG);
+
+ ProjectConfig config = ProjectConfig.read(md);
+ AccessSection groups = config.getAccessSection(RefNames.REFS_GROUPS + "*", true);
+ grant(
+ config,
+ groups,
+ Permission.READ,
+ false,
+ true,
+ systemGroupBackend.getGroup(REGISTERED_USERS));
+ config.commit(md);
+ } catch (IOException | ConfigInvalidException e) {
+ throw new OrmException("Failed to grant read permissions on group branches", e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/update/RefUpdateUtil.java b/java/com/google/gerrit/server/update/RefUpdateUtil.java
index 17c7812..166f288 100644
--- a/java/com/google/gerrit/server/update/RefUpdateUtil.java
+++ b/java/com/google/gerrit/server/update/RefUpdateUtil.java
@@ -20,6 +20,7 @@
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
@@ -28,7 +29,25 @@
/**
* Execute a batch ref update, throwing a checked exception if not all updates succeeded.
*
+ * <p>Creates a new {@link RevWalk} used only for this operation.
+ *
* @param bru batch update; should already have been executed.
+ * @param repo repository that created {@code bru}.
+ * @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
+ * #checkResults(BatchRefUpdate)} for details.
+ * @throws IOException if any result was not {@code OK}.
+ */
+ public static void executeChecked(BatchRefUpdate bru, Repository repo) throws IOException {
+ try (RevWalk rw = new RevWalk(repo)) {
+ executeChecked(bru, rw);
+ }
+ }
+
+ /**
+ * Execute a batch ref update, throwing a checked exception if not all updates succeeded.
+ *
+ * @param bru batch update; should already have been executed.
+ * @param rw walk for executing the update.
* @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
* #checkResults(BatchRefUpdate)} for details.
* @throws IOException if any result was not {@code OK}.
diff --git a/java/com/google/gerrit/sshd/SshScope.java b/java/com/google/gerrit/sshd/SshScope.java
index 963a71a..2659831 100644
--- a/java/com/google/gerrit/sshd/SshScope.java
+++ b/java/com/google/gerrit/sshd/SshScope.java
@@ -102,7 +102,7 @@
synchronized Context subContext(SshSession newSession, String newCommandLine) {
Context ctx = new Context(this, newSession, newCommandLine);
- cleanup.add(ctx.cleanup);
+ ctx.cleanup.add(cleanup);
return ctx;
}
}
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index e8d20d0..8e13b61 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -23,6 +23,7 @@
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server:module",
+ "//java/com/google/gerrit/server/api",
"//java/com/google/gerrit/server/cache/h2",
"//lib:gwtorm",
"//lib:h2",
diff --git a/java/com/google/gerrit/testing/GroupNoteDbMode.java b/java/com/google/gerrit/testing/GroupNoteDbMode.java
new file mode 100644
index 0000000..9d8e557
--- /dev/null
+++ b/java/com/google/gerrit/testing/GroupNoteDbMode.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testing;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Enums;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.notedb.GroupsMigration;
+
+public enum GroupNoteDbMode {
+ /** NoteDb is disabled, groups are only in ReviewDb */
+ OFF(new GroupsMigration(false, false)),
+
+ /** Writing new groups to NoteDb is enabled. */
+ WRITE(new GroupsMigration(true, false)),
+
+ /**
+ * Reading/writing groups from/to NoteDb is enabled. Trying to read groups from ReviewDb throws an
+ * exception.
+ */
+ READ_WRITE(new GroupsMigration(true, true));
+
+ private static final String ENV_VAR = "GERRIT_NOTEDB_GROUPS";
+ private static final String SYS_PROP = "gerrit.notedb.groups";
+
+ public static GroupNoteDbMode get() {
+ String value = System.getenv(ENV_VAR);
+ if (Strings.isNullOrEmpty(value)) {
+ value = System.getProperty(SYS_PROP);
+ }
+ if (Strings.isNullOrEmpty(value)) {
+ return OFF;
+ }
+ value = value.toUpperCase().replace("-", "_");
+ GroupNoteDbMode mode = Enums.getIfPresent(GroupNoteDbMode.class, value).orNull();
+ if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) {
+ checkArgument(
+ mode != null, "Invalid value for env variable %s: %s", ENV_VAR, System.getenv(ENV_VAR));
+ } else {
+ checkArgument(
+ mode != null,
+ "Invalid value for system property %s: %s",
+ SYS_PROP,
+ System.getProperty(SYS_PROP));
+ }
+ return mode;
+ }
+
+ private final GroupsMigration groupsMigration;
+
+ private GroupNoteDbMode(GroupsMigration groupsMigration) {
+ this.groupsMigration = groupsMigration;
+ }
+
+ public GroupsMigration getGroupsMigration() {
+ return groupsMigration;
+ }
+}
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index b5c2187..72629c7 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -29,6 +29,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllProjectsNameProvider;
@@ -161,6 +162,7 @@
});
bind(MetricMaker.class).to(DisabledMetricMaker.class);
install(cfgInjector.getInstance(GerritGlobalModule.class));
+ install(new GerritApiModule());
install(new DefaultPermissionBackendModule());
install(new SearchingChangeCacheImpl.Module());
factory(GarbageCollection.Factory.class);
diff --git a/java/gerrit/PRED_commit_edits_2.java b/java/gerrit/PRED_commit_edits_2.java
index 0490429..c196026 100644
--- a/java/gerrit/PRED_commit_edits_2.java
+++ b/java/gerrit/PRED_commit_edits_2.java
@@ -96,7 +96,10 @@
if (fileRegex.matcher(newName).find()
|| (oldName != null && fileRegex.matcher(oldName).find())) {
- List<Edit> edits = entry.getEdits();
+ // This cast still seems to be needed on JDK 8 as workaround for:
+ // https://bugs.openjdk.java.net/browse/JDK-8039214
+ @SuppressWarnings("cast")
+ List<Edit> edits = (List<Edit>) entry.getEdits();
if (edits.isEmpty()) {
continue;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index ce4d77b..c59a2f6 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -35,6 +35,7 @@
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import com.google.common.cache.LoadingCache;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -85,6 +86,7 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.AccountConfig;
+import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.account.WatchConfig;
@@ -94,6 +96,8 @@
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.index.account.AccountIndexer;
+import com.google.gerrit.server.index.account.StalenessChecker;
import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
import com.google.gerrit.server.project.RefPattern;
@@ -103,6 +107,7 @@
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.name.Named;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
@@ -114,6 +119,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.bouncycastle.bcpg.ArmoredOutputStream;
@@ -122,8 +128,12 @@
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
@@ -153,6 +163,8 @@
@Inject private ExternalIdsUpdate.User externalIdsUpdateFactory;
+ @Inject private ExternalIdsUpdate.ServerNoReindex externalIdsUpdateNoReindexFactory;
+
@Inject private DynamicSet<AccountIndexedListener> accountIndexedListeners;
@Inject private DynamicSet<GitReferenceUpdatedListener> refUpdateListeners;
@@ -163,6 +175,14 @@
@Inject protected Emails emails;
+ @Inject private StalenessChecker stalenessChecker;
+
+ @Inject private AccountIndexer accountIndexer;
+
+ @Inject
+ @Named("accounts")
+ private LoadingCache<Account.Id, Optional<AccountState>> accountsCache;
+
private AccountIndexedCounter accountIndexedCounter;
private RegistrationHandle accountIndexEventCounterHandle;
private RefUpdateCounter refUpdateCounter;
@@ -918,11 +938,11 @@
@Test
public void pushAccountConfigWithPrefEmailThatDoesNotExistAsExtIdToUserBranchForReviewAndSubmit()
throws Exception {
- TestAccount foo = accountCreator.create(name("foo"));
+ TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
String userRef = RefNames.refsUsers(foo.id);
accountIndexedCounter.clear();
- TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+ TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
fetch(allUsersRepo, userRef + ":userRef");
allUsersRepo.reset("userRef");
@@ -934,7 +954,7 @@
pushFactory
.create(
db,
- admin.getIdent(),
+ foo.getIdent(),
allUsersRepo,
"Update account config",
AccountConfig.ACCOUNT_CONFIG,
@@ -1058,7 +1078,10 @@
}
@Test
+ @Sandboxed
public void pushAccountConfigToUserBranchForReviewDeactivateOtherAccount() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestAccount foo = accountCreator.create(name("foo"));
assertThat(gApi.accounts().id(foo.id.get()).getActive()).isTrue();
String userRef = RefNames.refsUsers(foo.id);
@@ -1229,18 +1252,15 @@
@Test
public void pushAccountConfigToUserBranchInvalidPreferredEmailButNotChanged() throws Exception {
- TestAccount foo = accountCreator.create(name("foo"));
+ TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
String userRef = RefNames.refsUsers(foo.id);
String noEmail = "no.email";
accountsUpdate.create().update(foo.id, a -> a.setPreferredEmail(noEmail));
accountIndexedCounter.clear();
- InternalGroup adminGroup =
- groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
- grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
-
- TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+ grant(allUsers, userRef, Permission.PUSH, false, REGISTERED_USERS);
+ TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
fetch(allUsersRepo, userRef + ":userRef");
allUsersRepo.reset("userRef");
@@ -1251,7 +1271,7 @@
pushFactory
.create(
db,
- admin.getIdent(),
+ foo.getIdent(),
allUsersRepo,
"Update account config",
AccountConfig.ACCOUNT_CONFIG,
@@ -1268,7 +1288,7 @@
@Test
public void pushAccountConfigToUserBranchIfPreferredEmailDoesNotExistAsExtId() throws Exception {
- TestAccount foo = accountCreator.create(name("foo"));
+ TestAccount foo = accountCreator.create(name("foo"), name("foo") + "@example.com", "Foo");
String userRef = RefNames.refsUsers(foo.id);
accountIndexedCounter.clear();
@@ -1276,7 +1296,7 @@
groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
grant(allUsers, userRef, Permission.PUSH, false, adminGroup.getGroupUUID());
- TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+ TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, foo);
fetch(allUsersRepo, userRef + ":userRef");
allUsersRepo.reset("userRef");
@@ -1287,7 +1307,7 @@
pushFactory
.create(
db,
- admin.getIdent(),
+ foo.getIdent(),
allUsersRepo,
"Update account config",
AccountConfig.ACCOUNT_CONFIG,
@@ -1326,7 +1346,10 @@
}
@Test
+ @Sandboxed
public void pushAccountConfigToUserBranchDeactivateOtherAccount() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestAccount foo = accountCreator.create(name("foo"));
assertThat(gApi.accounts().id(foo.id.get()).getActive()).isTrue();
String userRef = RefNames.refsUsers(foo.id);
@@ -1776,6 +1799,75 @@
assertGroups(newUser, ImmutableList.of(group));
}
+ @Test
+ public void stalenessChecker() throws Exception {
+ // Newly created account is not stale.
+ AccountInfo accountInfo = gApi.accounts().create(name("foo")).get();
+ Account.Id accountId = new Account.Id(accountInfo._accountId);
+ assertThat(stalenessChecker.isStale(accountId)).isFalse();
+
+ // Manually updating the user ref makes the index document stale.
+ String userRef = RefNames.refsUsers(accountId);
+ try (Repository repo = repoManager.openRepository(allUsers);
+ ObjectInserter oi = repo.newObjectInserter();
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
+
+ PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
+ CommitBuilder cb = new CommitBuilder();
+ cb.setTreeId(commit.getTree());
+ cb.setCommitter(ident);
+ cb.setAuthor(ident);
+ cb.setMessage(commit.getFullMessage());
+ ObjectId emptyCommit = oi.insert(cb);
+ oi.flush();
+
+ RefUpdate updateRef = repo.updateRef(userRef);
+ updateRef.setExpectedOldObjectId(commit.toObjectId());
+ updateRef.setNewObjectId(emptyCommit);
+ assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ assertStaleAccountAndReindex(accountId);
+
+ // Manually inserting/updating/deleting an external ID of the user makes the index document
+ // stale.
+ ExternalIdsUpdate externalIdsUpdateNoReindex = externalIdsUpdateNoReindexFactory.create();
+ ExternalId.Key key = ExternalId.Key.create("foo", "foo");
+ externalIdsUpdateNoReindex.insert(ExternalId.create(key, accountId));
+ assertStaleAccountAndReindex(accountId);
+
+ externalIdsUpdateNoReindex.upsert(
+ ExternalId.createWithEmail(key, accountId, "foo@example.com"));
+ assertStaleAccountAndReindex(accountId);
+
+ externalIdsUpdateNoReindex.delete(accountId, key);
+ assertStaleAccountAndReindex(accountId);
+
+ // Manually delete account
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
+ RefUpdate updateRef = repo.updateRef(userRef);
+ updateRef.setExpectedOldObjectId(commit.toObjectId());
+ updateRef.setNewObjectId(ObjectId.zeroId());
+ updateRef.setForceUpdate(true);
+ assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ assertStaleAccountAndReindex(accountId);
+ }
+
+ private void assertStaleAccountAndReindex(Account.Id accountId) throws IOException {
+ // Evict account from cache to be sure that we use the index state for staleness checks. This
+ // has to happen directly on the accounts cache because AccountCacheImpl triggers a reindex for
+ // the account.
+ accountsCache.invalidate(accountId);
+ assertThat(stalenessChecker.isStale(accountId)).isTrue();
+
+ // Reindex fixes staleness
+ accountIndexer.index(accountId);
+ assertThat(stalenessChecker.isStale(accountId)).isFalse();
+ }
+
private void assertGroups(String user, List<String> expected) throws Exception {
List<String> actual =
gApi.accounts().id(user).getGroups().stream().map(g -> g.name).collect(toList());
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupRebuilderIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupRebuilderIT.java
index 3437cf4..9f98895 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupRebuilderIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupRebuilderIT.java
@@ -15,26 +15,24 @@
package com.google.gerrit.acceptance.api.group;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.READ;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.git.CommitUtil;
-import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.ServerInitiated;
import com.google.gerrit.server.group.db.GroupBundle;
-import com.google.gerrit.server.group.db.GroupConfig;
import com.google.gerrit.server.group.db.GroupRebuilder;
-import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.TestTimeUtil;
@@ -44,7 +42,6 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
-import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -64,15 +61,15 @@
public static Config defaultConfig() {
Config config = new Config();
// This test is explicitly testing the migration from ReviewDb to NoteDb, and handles reading
- // from NoteDb manually. It should work regardless of the value of writeGroupsToNoteDb, however.
- config.setBoolean("user", null, "readGroupsFromNoteDb", false);
+ // from NoteDb manually. It should work regardless of the value of noteDb.groups.write, however.
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, false);
return config;
}
@Inject @GerritServerId private String serverId;
@Inject @ServerInitiated private Provider<GroupsUpdate> groupsUpdate;
+ @Inject private GroupBundle.Factory bundleFactory;
@Inject private GroupRebuilder rebuilder;
- @Inject private Groups groups;
@Before
public void setTimeForTesting() {
@@ -87,10 +84,13 @@
@Test
public void basicGroupProperties() throws Exception {
GroupInfo createdGroup = gApi.groups().create(name("group")).get();
- InternalGroup reviewDbGroup = groups.getGroup(db, new AccountGroup.UUID(createdGroup.id)).get();
- deleteGroupRefs(reviewDbGroup);
+ try (BlockReviewDbUpdatesForGroups ctx = new BlockReviewDbUpdatesForGroups()) {
+ GroupBundle reviewDbBundle =
+ bundleFactory.fromReviewDb(db, new AccountGroup.Id(createdGroup.groupId));
+ deleteGroupRefs(reviewDbBundle);
- assertThat(rebuild(reviewDbGroup)).isEqualTo(roundToSecond(reviewDbGroup));
+ assertThat(rebuild(reviewDbBundle)).isEqualTo(reviewDbBundle.roundToSecond());
+ }
}
@Test
@@ -105,51 +105,54 @@
gApi.groups().id(group1.id).addGroups(group2.id);
- InternalGroup reviewDbGroup = groups.getGroup(db, new AccountGroup.UUID(group1.id)).get();
- deleteGroupRefs(reviewDbGroup);
+ try (BlockReviewDbUpdatesForGroups ctx = new BlockReviewDbUpdatesForGroups()) {
+ GroupBundle reviewDbBundle =
+ bundleFactory.fromReviewDb(db, new AccountGroup.Id(group1.groupId));
+ deleteGroupRefs(reviewDbBundle);
- InternalGroup noteDbGroup = rebuild(reviewDbGroup);
- assertThat(noteDbGroup).isEqualTo(roundToSecond(reviewDbGroup));
+ GroupBundle noteDbBundle = rebuild(reviewDbBundle);
+ assertThat(noteDbBundle).isEqualTo(reviewDbBundle.roundToSecond());
- ImmutableList<CommitInfo> log = log(group1);
- assertThat(log).hasSize(4);
+ ImmutableList<CommitInfo> log = log(group1);
+ assertThat(log).hasSize(4);
- assertThat(log.get(0)).message().isEqualTo("Create group");
- assertThat(log.get(0)).author().name().isEqualTo(serverIdent.get().getName());
- assertThat(log.get(0)).author().email().isEqualTo(serverIdent.get().getEmailAddress());
- assertThat(log.get(0)).author().date().isEqualTo(noteDbGroup.getCreatedOn());
- assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.get().getTimeZoneOffset());
- assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
+ assertThat(log.get(0)).message().isEqualTo("Create group");
+ assertThat(log.get(0)).author().name().isEqualTo(serverIdent.get().getName());
+ assertThat(log.get(0)).author().email().isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(log.get(0)).author().date().isEqualTo(noteDbBundle.group().getCreatedOn());
+ assertThat(log.get(0)).author().tz().isEqualTo(serverIdent.get().getTimeZoneOffset());
+ assertThat(log.get(0)).committer().isEqualTo(log.get(0).author);
- assertThat(log.get(1))
- .message()
- .isEqualTo("Update group\n\nAdd: Administrator <" + admin.id + "@" + serverId + ">");
- assertThat(log.get(1)).author().name().isEqualTo(admin.fullName);
- assertThat(log.get(1)).author().email().isEqualTo(admin.id + "@" + serverId);
- assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
+ assertThat(log.get(1))
+ .message()
+ .isEqualTo("Update group\n\nAdd: Administrator <" + admin.id + "@" + serverId + ">");
+ assertThat(log.get(1)).author().name().isEqualTo(admin.fullName);
+ assertThat(log.get(1)).author().email().isEqualTo(admin.id + "@" + serverId);
+ assertThat(log.get(1)).committer().hasSameDateAs(log.get(1).author);
- assertThat(log.get(2))
- .message()
- .isEqualTo(
- "Update group\n"
- + "\n"
- + ("Add: User <" + user.id + "@" + serverId + ">\n")
- + ("Add: User2 <" + user2.id + "@" + serverId + ">"));
- assertThat(log.get(2)).author().name().isEqualTo(admin.fullName);
- assertThat(log.get(2)).author().email().isEqualTo(admin.id + "@" + serverId);
- assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
+ assertThat(log.get(2))
+ .message()
+ .isEqualTo(
+ "Update group\n"
+ + "\n"
+ + ("Add: User <" + user.id + "@" + serverId + ">\n")
+ + ("Add: User2 <" + user2.id + "@" + serverId + ">"));
+ assertThat(log.get(2)).author().name().isEqualTo(admin.fullName);
+ assertThat(log.get(2)).author().email().isEqualTo(admin.id + "@" + serverId);
+ assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
- assertThat(log.get(3))
- .message()
- .isEqualTo("Update group\n\nAdd-group: " + group2.name + " <" + group2.id + ">");
- assertThat(log.get(3)).author().name().isEqualTo(admin.fullName);
- assertThat(log.get(3)).author().email().isEqualTo(admin.id + "@" + serverId);
- assertThat(log.get(3)).committer().hasSameDateAs(log.get(3).author);
+ assertThat(log.get(3))
+ .message()
+ .isEqualTo("Update group\n\nAdd-group: " + group2.name + " <" + group2.id + ">");
+ assertThat(log.get(3)).author().name().isEqualTo(admin.fullName);
+ assertThat(log.get(3)).author().email().isEqualTo(admin.id + "@" + serverId);
+ assertThat(log.get(3)).committer().hasSameDateAs(log.get(3).author);
+ }
}
- private void deleteGroupRefs(InternalGroup group) throws Exception {
+ private void deleteGroupRefs(GroupBundle bundle) throws Exception {
try (Repository repo = repoManager.openRepository(allUsers)) {
- String refName = RefNames.refsGroups(group.getGroupUUID());
+ String refName = RefNames.refsGroups(bundle.uuid());
RefUpdate ru = repo.updateRef(refName);
ru.setForceUpdate(true);
Ref oldRef = repo.exactRef(refName);
@@ -162,30 +165,13 @@
}
}
- private InternalGroup rebuild(InternalGroup group) throws Exception {
+ private GroupBundle rebuild(GroupBundle reviewDbBundle) throws Exception {
try (Repository repo = repoManager.openRepository(allUsers)) {
- rebuilder.rebuild(repo, GroupBundle.fromReviewDb(db, group.getId()));
- GroupConfig groupConfig = GroupConfig.loadForGroup(repo, group.getGroupUUID());
- Optional<InternalGroup> result = groupConfig.getLoadedGroup();
- assertThat(result).isPresent();
- return result.get();
+ rebuilder.rebuild(repo, reviewDbBundle, null);
+ return bundleFactory.fromNoteDb(repo, reviewDbBundle.uuid());
}
}
- private InternalGroup roundToSecond(InternalGroup g) {
- return InternalGroup.builder()
- .setId(g.getId())
- .setNameKey(g.getNameKey())
- .setDescription(g.getDescription())
- .setOwnerGroupUUID(g.getOwnerGroupUUID())
- .setVisibleToAll(g.isVisibleToAll())
- .setGroupUUID(g.getGroupUUID())
- .setCreatedOn(TimeUtil.roundToSecond(g.getCreatedOn()))
- .setMembers(g.getMembers())
- .setSubgroups(g.getSubgroups())
- .build();
- }
-
private ImmutableList<CommitInfo> log(GroupInfo g) throws Exception {
ImmutableList.Builder<CommitInfo> result = ImmutableList.builder();
List<Date> commitDates = new ArrayList<>();
@@ -205,4 +191,19 @@
assertThat(commitDates).named("commit timestamps for %s", result).isOrdered();
return result.build();
}
+
+ private class BlockReviewDbUpdatesForGroups implements AutoCloseable {
+ BlockReviewDbUpdatesForGroups() {
+ blockReviewDbUpdates(true);
+ }
+
+ @Override
+ public void close() throws Exception {
+ blockReviewDbUpdates(false);
+ }
+
+ private void blockReviewDbUpdates(boolean block) {
+ cfg.setBoolean("user", null, "blockReviewDbGroupUpdates", block);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index c090558..ba92835 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -22,8 +22,14 @@
import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.READ;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
import static java.util.stream.Collectors.toList;
+import com.google.common.base.Throwables;
+import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -33,8 +39,13 @@
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.groups.GroupApi;
import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -50,19 +61,27 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupIncludeCache;
+import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.group.db.GroupConfig;
import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.gerrit.server.index.group.StalenessChecker;
import com.google.gerrit.server.util.MagicBranch;
import com.google.gerrit.testing.ConfigSuite;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.name.Named;
import java.io.IOException;
import java.sql.Timestamp;
import java.time.Instant;
@@ -73,6 +92,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -83,6 +103,7 @@
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
@@ -93,13 +114,20 @@
@ConfigSuite.Config
public static Config noteDbConfig() {
Config config = new Config();
- config.setBoolean("user", null, "writeGroupsToNoteDb", true);
- config.setBoolean("user", null, "readGroupsFromNoteDb", true);
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, true);
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, true);
return config;
}
@Inject private Groups groups;
@Inject private GroupIncludeCache groupIncludeCache;
+ @Inject private GroupBackend groupBackend;
+ @Inject private StalenessChecker stalenessChecker;
+ @Inject private GroupIndexer groupIndexer;
+
+ @Inject
+ @Named("groups_byuuid")
+ private LoadingCache<String, Optional<InternalGroup>> groupsByUUIDCache;
@Test
public void systemGroupCanBeRetrievedFromIndex() throws Exception {
@@ -750,7 +778,6 @@
@Test
public void getAuditLog() throws Exception {
- assume().that(cfg.getBoolean("user", null, "readGroupsFromNoteDb", false)).isFalse();
GroupApi g = gApi.groups().create(name("group"));
List<? extends GroupAuditEventInfo> auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(1);
@@ -778,10 +805,30 @@
assertThat(auditEvents).hasSize(5);
assertAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup);
+ /**
+ * Make sure the new commit is created in a different second. This is added for NoteDb since the
+ * resolution of Timestamp is 1s there. Adding here is enough because the sort used in {@code
+ * GetAuditLog} is stable and we process {@code AccountGroupMemberAudit} before {@code
+ * AccountGroupByIdAud}.
+ */
+ Thread.sleep(1000);
+
+ // Add a removed member back again.
+ g.addMembers(user.username);
+ auditEvents = g.auditLog();
+ assertThat(auditEvents).hasSize(6);
+ assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
+
+ // Add a removed group back again.
+ g.addGroups(otherGroup);
+ auditEvents = g.auditLog();
+ assertThat(auditEvents).hasSize(7);
+ assertAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
+
Timestamp lastDate = null;
for (GroupAuditEventInfo auditEvent : auditEvents) {
if (lastDate != null) {
- assertThat(lastDate).isGreaterThan(auditEvent.date);
+ assertThat(lastDate).isAtLeast(auditEvent.date);
}
lastDate = auditEvent.date;
}
@@ -815,44 +862,57 @@
@Test
public void pushToGroupBranchIsRejectedForAllUsersRepo() throws Exception {
- pushToGroupBranch(allUsers, "Not allowed to create group branch.", "group update not allowed");
+ String groupRef =
+ RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("fo")).get().id));
+ assertPushToGroupBranch(allUsers, groupRef, !groupsInNoteDb(), "group update not allowed");
}
@Test
- public void pushToGroupBranchForNonAllUsersRepo() throws Exception {
- pushToGroupBranch(project, null, null);
+ public void pushToGroupNamesBranchIsRejectedForAllUsersRepo() throws Exception {
+ assume().that(groupsInNoteDb()).isTrue(); // branch only exists when groups are in NoteDb
+ // refs/meta/group-names isn't usually available for fetch, so grant ACCESS_DATABASE
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ assertPushToGroupBranch(allUsers, RefNames.REFS_GROUPNAMES, false, "group update not allowed");
}
- private void pushToGroupBranch(
- Project.NameKey project, String expectedErrorOnCreate, String expectedErrorOnUpdate)
+ @Test
+ public void pushToGroupsBranchForNonAllUsersRepo() throws Exception {
+ assertCreateGroupBranch(project, null);
+ String groupRef =
+ RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("fo")).get().id));
+ assertPushToGroupBranch(project, groupRef, true, null);
+ }
+
+ @Test
+ public void pushToGroupNamesBranchForNonAllUsersRepo() throws Exception {
+ assertPushToGroupBranch(project, RefNames.REFS_GROUPNAMES, true, null);
+ }
+
+ private void assertPushToGroupBranch(
+ Project.NameKey project, String groupRefName, boolean createRef, String expectedErrorOnUpdate)
throws Exception {
grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
+ grant(project, RefNames.REFS_GROUPNAMES, Permission.PUSH, false, REGISTERED_USERS);
TestRepository<InMemoryRepository> repo = cloneProject(project);
- // create new branch
- PushOneCommit.Result r =
- pushFactory
- .create(
- db, admin.getIdent(), repo, "Update group config", "group.config", "some content")
- .setParents(ImmutableList.of())
- .to(RefNames.REFS_GROUPS + name("foo"));
- if (expectedErrorOnCreate != null) {
- r.assertErrorStatus(expectedErrorOnCreate);
- } else {
- r.assertOkStatus();
+ if (createRef) {
+ createGroupBranch(project, groupRefName);
}
// update existing branch
- String groupRefName = RefNames.REFS_GROUPS + name("bar");
- createGroupBranch(project, groupRefName);
fetch(repo, groupRefName + ":groupRef");
repo.reset("groupRef");
- r =
+ PushOneCommit.Result r =
pushFactory
.create(
- db, admin.getIdent(), repo, "Update group config", "group.config", "some content")
+ db,
+ admin.getIdent(),
+ repo,
+ "Update group config",
+ GroupConfig.GROUP_CONFIG_FILE,
+ "some content")
.to(groupRefName);
if (expectedErrorOnUpdate != null) {
r.assertErrorStatus(expectedErrorOnUpdate);
@@ -861,6 +921,29 @@
}
}
+ private void assertCreateGroupBranch(Project.NameKey project, String expectedErrorOnCreate)
+ 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);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(
+ db,
+ admin.getIdent(),
+ repo,
+ "Update group config",
+ GroupConfig.GROUP_CONFIG_FILE,
+ "some content")
+ .setParents(ImmutableList.of())
+ .to(RefNames.REFS_GROUPS + name("bar"));
+ if (expectedErrorOnCreate != null) {
+ r.assertErrorStatus(expectedErrorOnCreate);
+ } else {
+ r.assertOkStatus();
+ }
+ }
+
@Test
public void pushToGroupBranchForReviewForAllUsersRepoIsRejectedOnSubmit() throws Exception {
pushToGroupBranchForReviewAndSubmit(allUsers, "group update not allowed");
@@ -935,6 +1018,129 @@
}
}
+ @Test
+ public void defaultPermissionsOnGroupBranches() throws Exception {
+ assertPermission(
+ allUsers, RefNames.REFS_GROUPS + "*", Permission.READ, groupRef(REGISTERED_USERS));
+ }
+
+ @Test
+ @Sandboxed
+ public void blockReviewDbUpdatesOnGroupCreation() throws Exception {
+ assume().that(groupsInNoteDb()).isFalse();
+ cfg.setBoolean("user", null, "blockReviewDbGroupUpdates", true);
+ try {
+ gApi.groups().create(name("foo"));
+ fail("Expected RestApiException: Updates to groups in ReviewDb are blocked");
+ } catch (RestApiException e) {
+ assertWriteGroupToReviewDbBlockedException(e);
+ }
+ }
+
+ @Test
+ @Sandboxed
+ public void blockReviewDbUpdatesOnGroupUpdate() throws Exception {
+ assume().that(groupsInNoteDb()).isFalse();
+ String group1 = gApi.groups().create(name("foo")).get().id;
+ String group2 = gApi.groups().create(name("bar")).get().id;
+ cfg.setBoolean("user", null, "blockReviewDbGroupUpdates", true);
+ try {
+ gApi.groups().id(group1).addGroups(group2);
+ fail("Expected RestApiException: Updates to groups in ReviewDb are blocked");
+ } catch (RestApiException e) {
+ assertWriteGroupToReviewDbBlockedException(e);
+ }
+ }
+
+ private void assertWriteGroupToReviewDbBlockedException(Exception e) throws Exception {
+ Throwable t = Throwables.getRootCause(e);
+ assertThat(t).isInstanceOf(OrmException.class);
+ assertThat(t.getMessage()).isEqualTo("Updates to groups in ReviewDb are blocked");
+ }
+
+ private GroupReference groupRef(AccountGroup.UUID groupUuid) {
+ GroupDescription.Basic groupDescription = groupBackend.get(groupUuid);
+ return new GroupReference(groupDescription.getGroupUUID(), groupDescription.getName());
+ }
+
+ private void assertPermission(
+ Project.NameKey project, String ref, String permission, GroupReference groupReference)
+ throws IOException {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ AccessSection accessSection = cfg.getAccessSection(ref);
+ assertThat(accessSection).isNotNull();
+ Permission readPermission = accessSection.getPermission(permission);
+ assertThat(readPermission).isNotNull();
+ assertThat(readPermission.getName()).isEqualTo(permission);
+ assertThat(readPermission.getExclusiveGroup()).isTrue();
+ assertThat(readPermission.getLabel()).isNull();
+
+ PermissionRule rule = readPermission.getRule(groupReference);
+ assertThat(rule).isNotNull();
+ assertThat(rule.getGroup()).isEqualTo(groupReference);
+ assertThat(rule.getAction()).isEqualTo(Action.ALLOW);
+ assertThat(rule.getForce()).isFalse();
+ assertThat(rule.getMin()).isEqualTo(0);
+ assertThat(rule.getMax()).isEqualTo(0);
+ }
+
+ @Test
+ public void stalenessChecker() throws Exception {
+ assume().that(groupsInNoteDb()).isTrue();
+
+ // Newly created group is not stale
+ GroupInfo groupInfo = gApi.groups().create(name("foo")).get();
+ AccountGroup.UUID groupUuid = new AccountGroup.UUID(groupInfo.id);
+ assertThat(stalenessChecker.isStale(groupUuid)).isFalse();
+
+ // Manual update makes index document stale
+ String groupRef = RefNames.refsGroups(groupUuid);
+ try (Repository repo = repoManager.openRepository(allUsers);
+ ObjectInserter oi = repo.newObjectInserter();
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(repo.exactRef(groupRef).getObjectId());
+
+ PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
+ CommitBuilder cb = new CommitBuilder();
+ cb.setTreeId(commit.getTree());
+ cb.setCommitter(ident);
+ cb.setAuthor(ident);
+ cb.setMessage(commit.getFullMessage());
+ ObjectId emptyCommit = oi.insert(cb);
+ oi.flush();
+
+ RefUpdate updateRef = repo.updateRef(groupRef);
+ updateRef.setExpectedOldObjectId(commit.toObjectId());
+ updateRef.setNewObjectId(emptyCommit);
+ assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ assertStaleGroupAndReindex(groupUuid);
+
+ // Manually delete group
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(repo.exactRef(groupRef).getObjectId());
+ RefUpdate updateRef = repo.updateRef(groupRef);
+ updateRef.setExpectedOldObjectId(commit.toObjectId());
+ updateRef.setNewObjectId(ObjectId.zeroId());
+ updateRef.setForceUpdate(true);
+ assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ assertStaleGroupAndReindex(groupUuid);
+ }
+
+ private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException {
+ // Evict group from cache to be sure that we use the index state for staleness checks. This has
+ // to happen directly on the groupsByUUID cache because GroupsCacheImpl triggers a reindex for
+ // the group.
+ groupsByUUIDCache.invalidate(groupUuid.get());
+ assertThat(stalenessChecker.isStale(groupUuid)).isTrue();
+
+ // Reindex fixes staleness
+ groupIndexer.index(groupUuid);
+ assertThat(stalenessChecker.isStale(groupUuid)).isFalse();
+ }
+
private void pushToGroupBranchForReviewAndSubmit(Project.NameKey project, String expectedError)
throws Exception {
grantLabel(
@@ -1053,7 +1259,7 @@
}
private boolean groupsInNoteDb() {
- return cfg.getBoolean("user", "writeGroupsToNoteDb", false)
- && cfg.getBoolean("user", "readGroupsFromNoteDb", false);
+ return cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, false)
+ && cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, false);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index a6dcc62..0adfafd 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -17,25 +17,34 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.util.stream.Collectors.toList;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.git.ProjectConfig;
@@ -45,14 +54,16 @@
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testing.NoteDbMode;
import com.google.gerrit.testing.TestChanges;
import com.google.inject.Inject;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.function.Predicate;
import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.LsRemoteCommand;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
@@ -70,6 +81,7 @@
@Inject private AllUsersName allUsersName;
private AccountGroup.UUID admins;
+ private AccountGroup.UUID nonInteractiveUsers;
private ChangeData c1;
private ChangeData c2;
@@ -83,19 +95,32 @@
@Before
public void setUp() throws Exception {
admins = groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID();
+ nonInteractiveUsers =
+ groupCache
+ .get(new AccountGroup.NameKey("Non-Interactive Users"))
+ .orElse(null)
+ .getGroupUUID();
setUpPermissions();
setUpChanges();
}
private void setUpPermissions() throws Exception {
- // Remove read permissions for all users besides admin. This method is
- // idempotent, so is safe to call on every test setup.
+ // Remove read permissions for all users besides admin. This method is idempotent, so is safe
+ // to call on every test setup.
ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
for (AccessSection sec : pc.getAccessSections()) {
sec.removePermission(Permission.READ);
}
Util.allow(pc, Permission.READ, admins, "refs/*");
saveProjectConfig(allProjects, pc);
+
+ // Remove all read permissions on All-Users. This method is idempotent, so is safe to call on
+ // every test setup.
+ pc = projectCache.checkedGet(allUsers).getConfig();
+ for (AccessSection sec : pc.getAccessSections()) {
+ sec.removePermission(Permission.READ);
+ }
+ saveProjectConfig(allUsers, pc);
}
private static String changeRefPrefix(Change.Id id) {
@@ -447,22 +472,123 @@
}
@Test
+ public void advertisedReferencesDontShowUserBranchWithoutRead() throws Exception {
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getUserRefs(git)).isEmpty();
+ }
+ }
+
+ @Test
+ public void advertisedReferencesOmitUserBranchesOfOtherUsers() throws Exception {
+ allow(allUsersName, RefNames.REFS_USERS + "*", Permission.READ, REGISTERED_USERS);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getUserRefs(git))
+ .containsExactly(RefNames.REFS_USERS_SELF, RefNames.refsUsers(user.id));
+ }
+ }
+
+ @Test
+ public void advertisedReferencesIncludeAllUserBranchesWithAccessDatabase() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ try {
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getUserRefs(git))
+ .containsExactly(
+ RefNames.REFS_USERS_SELF,
+ RefNames.refsUsers(user.id),
+ RefNames.refsUsers(admin.id));
+ }
+ } finally {
+ removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ }
+ }
+
+ @Test
+ @Sandboxed
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesDontShowGroupBranchToOwnerWithoutRead() throws Exception {
+ createSelfOwnedGroup("Foos", user);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getGroupRefs(git)).isEmpty();
+ }
+ }
+
+ @Test
+ @Sandboxed
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesOmitGroupBranchesOfNonOwnedGroups() throws Exception {
+ allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
+ AccountGroup.UUID users = createGroup("Users", admins, user);
+ AccountGroup.UUID foos = createGroup("Foos", users);
+ AccountGroup.UUID bars = createSelfOwnedGroup("Bars", user);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getGroupRefs(git))
+ .containsExactly(RefNames.refsGroups(foos), RefNames.refsGroups(bars));
+ }
+ }
+
+ @Test
+ @Sandboxed
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesIncludeAllGroupBranchesWithAccessDatabase() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ AccountGroup.UUID users = createGroup("Users", admins);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getGroupRefs(git))
+ .containsExactly(
+ RefNames.refsGroups(admins),
+ RefNames.refsGroups(nonInteractiveUsers),
+ RefNames.refsGroups(users));
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesIncludeAllGroupBranchesForAdmins() throws Exception {
+ allow(allUsersName, RefNames.REFS_GROUPS + "*", Permission.READ, REGISTERED_USERS);
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
+ try {
+ AccountGroup.UUID users = createGroup("Users", admins);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getGroupRefs(git))
+ .containsExactly(
+ RefNames.refsGroups(admins),
+ RefNames.refsGroups(nonInteractiveUsers),
+ RefNames.refsGroups(users));
+ }
+ } finally {
+ removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ADMINISTRATE_SERVER);
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void advertisedReferencesOmitNoteDbNotesBranches() throws Exception {
+ allow(allUsersName, RefNames.REFS + "*", Permission.READ, REGISTERED_USERS);
+ TestRepository<?> userTestRepository = cloneProject(allUsers, user);
+ try (Git git = userTestRepository.git()) {
+ assertThat(getRefs(git)).containsNoneOf(RefNames.REFS_EXTERNAL_IDS, RefNames.REFS_GROUPNAMES);
+ }
+ }
+
+ @Test
public void advertisedReferencesOmitPrivateChangesOfOtherUsers() throws Exception {
allow("refs/heads/master", Permission.READ, REGISTERED_USERS);
TestRepository<?> userTestRepository = cloneProject(project, user);
try (Git git = userTestRepository.git()) {
- LsRemoteCommand lsRemoteCommand = git.lsRemote();
String change3RefName = c3.currentPatchSet().getRefName();
-
- List<String> initialRefNames =
- lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
- assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
+ assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
gApi.changes().id(c3.getId().get()).setPrivate(true, null);
-
- List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
- assertThat(refNames).doesNotContain(change3RefName);
+ assertThat(getRefs(git)).doesNotContain(change3RefName);
}
}
@@ -472,21 +598,16 @@
TestRepository<?> userTestRepository = cloneProject(project, user);
try (Git git = userTestRepository.git()) {
- LsRemoteCommand lsRemoteCommand = git.lsRemote();
String change3RefName = c3.currentPatchSet().getRefName();
-
- List<String> initialRefNames =
- lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
- assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
+ assertWithMessage("Precondition violated").that(getRefs(git)).contains(change3RefName);
gApi.changes().id(c3.getId().get()).setPrivate(true, null);
-
- List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
- assertThat(refNames).contains(change3RefName);
+ assertThat(getRefs(git)).contains(change3RefName);
}
}
@Test
+ @Sandboxed
public void advertisedReferencesOmitDraftCommentRefsOfOtherUsers() throws Exception {
assume().that(notesMigration.commitChangeWrites()).isTrue();
@@ -509,6 +630,7 @@
}
@Test
+ @Sandboxed
public void advertisedReferencesOmitStarredChangesRefsOfOtherUsers() throws Exception {
assume().that(notesMigration.commitChangeWrites()).isTrue();
@@ -526,6 +648,57 @@
assertThat(lsRemote(allUsersName, accountCreator.user2())).doesNotContain(starredChangesRef);
}
+ @Test
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
+ public void hideMetadata() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ try {
+ // create change
+ TestRepository<?> allUsersRepo = cloneProject(allUsers);
+ fetch(allUsersRepo, RefNames.REFS_USERS_SELF + ":userRef");
+ allUsersRepo.reset("userRef");
+ PushOneCommit.Result mr =
+ pushFactory
+ .create(db, admin.getIdent(), allUsersRepo)
+ .to("refs/for/" + RefNames.REFS_USERS_SELF);
+ mr.assertOkStatus();
+
+ List<String> expectedNonMetaRefs =
+ ImmutableList.of(
+ RefNames.REFS_USERS_SELF,
+ RefNames.refsUsers(admin.id),
+ RefNames.refsUsers(user.id),
+ RefNames.REFS_EXTERNAL_IDS,
+ RefNames.REFS_GROUPNAMES,
+ RefNames.refsGroups(admins),
+ RefNames.refsGroups(nonInteractiveUsers),
+ RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS,
+ RefNames.REFS_SEQUENCES + Sequences.NAME_GROUPS,
+ RefNames.REFS_CONFIG);
+
+ List<String> expectedMetaRefs =
+ new ArrayList<>(ImmutableList.of(mr.getPatchSetId().toRefName()));
+ if (NoteDbMode.get() != NoteDbMode.OFF) {
+ expectedMetaRefs.add(changeRefPrefix(mr.getChange().getId()) + "meta");
+ }
+
+ List<String> expectedAllRefs = new ArrayList<>(expectedNonMetaRefs);
+ expectedAllRefs.addAll(expectedMetaRefs);
+
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ Map<String, Ref> all = repo.getAllRefs();
+
+ VisibleRefFilter filter = refFilterFactory.create(projectCache.get(allUsers), repo);
+ assertThat(filter.filter(all, false).keySet()).containsExactlyElementsIn(expectedAllRefs);
+
+ assertThat(filter.setShowMetadata(false).filter(all, false).keySet())
+ .containsExactlyElementsIn(expectedNonMetaRefs);
+ }
+ } finally {
+ removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ }
+ }
+
private List<String> lsRemote(Project.NameKey p, TestAccount a) throws Exception {
TestRepository<?> testRepository = cloneProject(p, a);
try (Git git = testRepository.git()) {
@@ -533,6 +706,22 @@
}
}
+ private List<String> getRefs(Git git) throws Exception {
+ return getRefs(git, Predicates.alwaysTrue());
+ }
+
+ private List<String> getUserRefs(Git git) throws Exception {
+ return getRefs(git, RefNames::isRefsUsers);
+ }
+
+ private List<String> getGroupRefs(Git git) throws Exception {
+ return getRefs(git, RefNames::isRefsGroups);
+ }
+
+ private List<String> getRefs(Git git, Predicate<String> predicate) throws Exception {
+ return git.lsRemote().call().stream().map(Ref::getName).filter(predicate).collect(toList());
+ }
+
/**
* Assert that refs seen by a non-admin user match expected.
*
@@ -590,4 +779,20 @@
assertWithMessage("%s not found in %s", psId, cd.patchSets()).that(ps).isNotNull();
return ObjectId.fromString(ps.getRevision().get());
}
+
+ private AccountGroup.UUID createSelfOwnedGroup(String name, TestAccount... members)
+ throws RestApiException {
+ return createGroup(name, null, members);
+ }
+
+ private AccountGroup.UUID createGroup(
+ String name, @Nullable AccountGroup.UUID ownerGroup, TestAccount... members)
+ throws RestApiException {
+ GroupInput groupInput = new GroupInput();
+ groupInput.name = name(name);
+ groupInput.ownerId = ownerGroup != null ? ownerGroup.get() : null;
+ groupInput.members =
+ Arrays.stream(members).map(m -> String.valueOf(m.id.get())).collect(toList());
+ return new AccountGroup.UUID(gApi.groups().create(groupInput).get().id);
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 4cc15d6..15e6a35 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -273,6 +273,8 @@
@Test
public void pushToExternalIdsBranch() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -296,6 +298,8 @@
@Test
public void pushToExternalIdsBranchRejectsExternalIdWithoutAccountId() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -312,6 +316,8 @@
@Test
public void pushToExternalIdsBranchRejectsExternalIdWithKeyThatDoesntMatchTheNoteId()
throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -327,6 +333,8 @@
@Test
public void pushToExternalIdsBranchRejectsExternalIdWithInvalidConfig() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -342,6 +350,8 @@
@Test
public void pushToExternalIdsBranchRejectsExternalIdWithEmptyNote() throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
@@ -380,6 +390,8 @@
private void testPushToExternalIdsBranchRejectsInvalidExternalId(ExternalId invalidExtId)
throws Exception {
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index b150df3..ba845e5 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -103,7 +103,7 @@
}
@Test
- @GerritConfig(name = "user.writeGroupsToNoteDb", value = "true")
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
public void createGroupBranch_Conflict() throws Exception {
allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 3abc581..c8d92c7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -140,7 +140,7 @@
}
@Test
- @GerritConfig(name = "user.writeGroupsToNoteDb", value = "true")
+ @GerritConfig(name = "noteDb.groups.write", value = "true")
public void deleteGroupBranch_Conflict() throws Exception {
allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE, REGISTERED_USERS);
allow(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH, REGISTERED_USERS);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 2ade7f6..a9ea42e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -217,6 +217,11 @@
assertThatNameList(gApi.projects().list().withType(FilterType.ALL).get())
.containsExactly(allProjects, allUsers, project)
.inOrder();
+
+ // "All" boolean option causes hidden projects to be included
+ assertThatNameList(gApi.projects().list().withAll().get())
+ .containsExactly(allProjects, allUsers, project, hidden)
+ .inOrder();
}
private void assertBadRequest(ListRequest req) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
index adc0f20..90f20ef 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -454,15 +454,6 @@
}
@Test
- public void rebuildReviewDbRequiresNoteDbPrimary() throws Exception {
- Change.Id id = createChange().getChange().getId();
-
- exception.expect(OrmException.class);
- exception.expectMessage("primary storage of " + id + " is REVIEW_DB");
- rebuilderWrapper.rebuildReviewDb(db, project, id);
- }
-
- @Test
public void migrateBackToReviewDbPrimary() throws Exception {
Change c = createChange().getChange().change();
Change.Id id = c.getId();
diff --git a/javatests/com/google/gerrit/server/git/VersionedMetaDataTest.java b/javatests/com/google/gerrit/server/git/VersionedMetaDataTest.java
index a2b681d..d765c5e 100644
--- a/javatests/com/google/gerrit/server/git/VersionedMetaDataTest.java
+++ b/javatests/com/google/gerrit/server/git/VersionedMetaDataTest.java
@@ -190,9 +190,7 @@
assertMyMetaData(d2.getRefName(), 0);
assertThat(bru.getCommands().stream().map(ReceiveCommand::getRefName))
.containsExactly("refs/meta/1", "refs/meta/2");
- try (RevWalk rw = new RevWalk(repo)) {
- RefUpdateUtil.executeChecked(bru, rw);
- }
+ RefUpdateUtil.executeChecked(bru, repo);
assertMyMetaData(d1.getRefName(), 4, "Increment conf.value by 1", "Increment conf.value by 3");
assertMyMetaData(
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
new file mode 100644
index 0000000..70d5bf6
--- /dev/null
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2017 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.group.db;
+
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.CommitUtil;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import java.sql.Timestamp;
+import java.util.TimeZone;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+
+@Ignore
+public class AbstractGroupTest extends GerritBaseTests {
+ protected static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
+ protected static final String SERVER_ID = "server-id";
+ protected static final String SERVER_NAME = "Gerrit Server";
+ protected static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
+ protected static final int SERVER_ACCOUNT_NUMBER = 100000;
+ protected static final int USER_ACCOUNT_NUMBER = 100001;
+
+ protected AllUsersName allUsersName;
+ protected InMemoryRepositoryManager repoManager;
+ protected Repository allUsersRepo;
+ protected Account.Id serverAccountId;
+ protected PersonIdent serverIdent;
+ protected Account.Id userId;
+ protected PersonIdent userIdent;
+
+ @Before
+ public void abstractGroupTestSetUp() throws Exception {
+ allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
+ repoManager = new InMemoryRepositoryManager();
+ allUsersRepo = repoManager.createRepository(allUsersName);
+ serverAccountId = new Account.Id(SERVER_ACCOUNT_NUMBER);
+ serverIdent = new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+ userId = new Account.Id(USER_ACCOUNT_NUMBER);
+ userIdent = newPersonIdent(userId, serverIdent);
+ }
+
+ @After
+ public void abstractGroupTestTearDown() throws Exception {
+ allUsersRepo.close();
+ }
+
+ protected Timestamp getTipTimestamp(AccountGroup.UUID uuid) throws Exception {
+ try (RevWalk rw = new RevWalk(allUsersRepo)) {
+ Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(uuid));
+ return ref == null
+ ? null
+ : new Timestamp(rw.parseCommit(ref.getObjectId()).getAuthorIdent().getWhen().getTime());
+ }
+ }
+
+ protected void assertTipCommit(AccountGroup.UUID uuid, String expectedMessage) throws Exception {
+ try (RevWalk rw = new RevWalk(allUsersRepo)) {
+ Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(uuid));
+ assertCommit(
+ CommitUtil.toCommitInfo(rw.parseCommit(ref.getObjectId()), rw),
+ expectedMessage,
+ getAccountName(userId),
+ getAccountEmail(userId));
+ }
+ }
+
+ protected static void assertServerCommit(CommitInfo commitInfo, String expectedMessage) {
+ assertCommit(commitInfo, expectedMessage, SERVER_NAME, SERVER_EMAIL);
+ }
+
+ protected static void assertCommit(
+ CommitInfo commitInfo, String expectedMessage, String expectedName, String expectedEmail) {
+ assertThat(commitInfo).message().isEqualTo(expectedMessage);
+ assertThat(commitInfo).author().name().isEqualTo(expectedName);
+ assertThat(commitInfo).author().email().isEqualTo(expectedEmail);
+
+ // Committer should always be the server, regardless of author.
+ assertThat(commitInfo).committer().name().isEqualTo(SERVER_NAME);
+ assertThat(commitInfo).committer().email().isEqualTo(SERVER_EMAIL);
+ assertThat(commitInfo).committer().date().isEqualTo(commitInfo.author.date);
+ assertThat(commitInfo).committer().tz().isEqualTo(commitInfo.author.tz);
+ }
+
+ protected MetaDataUpdate createMetaDataUpdate(PersonIdent authorIdent) {
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo);
+ md.getCommitBuilder().setAuthor(authorIdent);
+ md.getCommitBuilder().setCommitter(serverIdent); // Committer is always the server identity.
+ return md;
+ }
+
+ protected static PersonIdent newPersonIdent() {
+ return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+ }
+
+ protected static PersonIdent newPersonIdent(Account.Id id, PersonIdent ident) {
+ return new PersonIdent(
+ getAccountName(id), getAccountEmail(id), ident.getWhen(), ident.getTimeZone());
+ }
+
+ protected static String getAccountNameEmail(Account.Id id) {
+ return String.format("%s <%s>", getAccountName(id), getAccountEmail(id));
+ }
+
+ protected static String getGroupName(AccountGroup.UUID uuid) {
+ return String.format("Group <%s>", uuid);
+ }
+
+ protected static String getAccountName(Account.Id id) {
+ return "Account " + id;
+ }
+
+ protected static String getAccountEmail(Account.Id id) {
+ return String.format("%s@%s", id, SERVER_ID);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
new file mode 100644
index 0000000..321580c
--- /dev/null
+++ b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
@@ -0,0 +1,381 @@
+// Copyright (C) 2017 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.group.db;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.git.CommitUtil;
+import com.google.gerrit.server.group.InternalGroup;
+import java.sql.Timestamp;
+import java.util.Set;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Unit tests for {@link AuditLogReader}. */
+public final class AuditLogReaderTest extends AbstractGroupTest {
+
+ private AuditLogReader auditLogReader;
+
+ @Before
+ public void setUp() throws Exception {
+ auditLogReader = new AuditLogReader(SERVER_ID, repoManager, allUsersName);
+ }
+
+ @Test
+ public void createGroupAsUserIdent() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ AccountGroupMemberAudit expAudit =
+ createExpMemberAudit(group.getId(), userId, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(uuid)).containsExactly(expAudit);
+ }
+
+ @Test
+ public void createGroupAsServerIdent() throws Exception {
+ InternalGroup group = createGroup(1, "test-group", serverIdent, null);
+ assertThat(auditLogReader.getMembersAudit(group.getGroupUUID())).hasSize(0);
+ }
+
+ @Test
+ public void addAndRemoveMember() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ AccountGroupMemberAudit expAudit1 =
+ createExpMemberAudit(group.getId(), userId, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(uuid)).containsExactly(expAudit1);
+
+ // User adds account 100002 to the group.
+ Account.Id id = new Account.Id(100002);
+ addMembers(uuid, ImmutableSet.of(id));
+
+ AccountGroupMemberAudit expAudit2 =
+ createExpMemberAudit(group.getId(), id, userId, getTipTimestamp(uuid));
+ assertTipCommit(uuid, "Update group\n\nAdd: Account 100002 <100002@server-id>");
+ assertThat(auditLogReader.getMembersAudit(uuid))
+ .containsExactly(expAudit1, expAudit2)
+ .inOrder();
+
+ // User removes account 100002 from the group.
+ removeMembers(uuid, ImmutableSet.of(id));
+ assertTipCommit(uuid, "Update group\n\nRemove: Account 100002 <100002@server-id>");
+
+ expAudit2.removed(userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(uuid))
+ .containsExactly(expAudit1, expAudit2)
+ .inOrder();
+ }
+
+ @Test
+ public void addMultiMembers() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.Id groupId = group.getId();
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ AccountGroupMemberAudit expAudit1 =
+ createExpMemberAudit(groupId, userId, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(uuid)).containsExactly(expAudit1);
+
+ Account.Id id1 = new Account.Id(100002);
+ Account.Id id2 = new Account.Id(100003);
+ addMembers(uuid, ImmutableSet.of(id1, id2));
+
+ AccountGroupMemberAudit expAudit2 =
+ createExpMemberAudit(groupId, id1, userId, getTipTimestamp(uuid));
+ AccountGroupMemberAudit expAudit3 =
+ createExpMemberAudit(groupId, id2, userId, getTipTimestamp(uuid));
+
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + "Add: Account 100002 <100002@server-id>\n"
+ + "Add: Account 100003 <100003@server-id>");
+ assertThat(auditLogReader.getMembersAudit(uuid))
+ .containsExactly(expAudit1, expAudit2, expAudit3)
+ .inOrder();
+ }
+
+ @Test
+ public void addAndRemoveSubgroups() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ InternalGroup subgroup = createGroupAsUser(2, "test-group-2");
+ AccountGroup.UUID subgroupUuid = subgroup.getGroupUUID();
+
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid));
+ assertTipCommit(uuid, String.format("Update group\n\nAdd-group: Group <%s>", subgroupUuid));
+
+ AccountGroupByIdAud expAudit =
+ createExpGroupAudit(group.getId(), subgroupUuid, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(uuid)).containsExactly(expAudit);
+
+ removeSubgroups(uuid, ImmutableSet.of(subgroupUuid));
+ assertTipCommit(uuid, String.format("Update group\n\nRemove-group: Group <%s>", subgroupUuid));
+
+ expAudit.removed(userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(uuid)).containsExactly(expAudit);
+ }
+
+ @Test
+ public void addMultiSubgroups() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.UUID uuid = group.getGroupUUID();
+
+ InternalGroup subgroup1 = createGroupAsUser(2, "test-group-2");
+ InternalGroup subgroup2 = createGroupAsUser(3, "test-group-3");
+ AccountGroup.UUID subgroupUuid1 = subgroup1.getGroupUUID();
+ AccountGroup.UUID subgroupUuid2 = subgroup2.getGroupUUID();
+
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid1, subgroupUuid2));
+
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Add-group: Group <%s>\n", subgroupUuid1)
+ + String.format("Add-group: Group <%s>", subgroupUuid2));
+
+ AccountGroupByIdAud expAudit1 =
+ createExpGroupAudit(group.getId(), subgroupUuid1, userId, getTipTimestamp(uuid));
+ AccountGroupByIdAud expAudit2 =
+ createExpGroupAudit(group.getId(), subgroupUuid2, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(uuid))
+ .containsExactly(expAudit1, expAudit2)
+ .inOrder();
+ }
+
+ @Test
+ public void addAndRemoveMembersAndSubgroups() throws Exception {
+ InternalGroup group = createGroupAsUser(1, "test-group");
+ AccountGroup.Id groupId = group.getId();
+ AccountGroup.UUID uuid = group.getGroupUUID();
+ AccountGroupMemberAudit expMemberAudit =
+ createExpMemberAudit(groupId, userId, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(uuid)).containsExactly(expMemberAudit);
+
+ Account.Id id1 = new Account.Id(100002);
+ Account.Id id2 = new Account.Id(100003);
+ Account.Id id3 = new Account.Id(100004);
+ InternalGroup subgroup1 = createGroupAsUser(2, "test-group-2");
+ InternalGroup subgroup2 = createGroupAsUser(3, "test-group-3");
+ InternalGroup subgroup3 = createGroupAsUser(4, "test-group-4");
+ AccountGroup.UUID subgroupUuid1 = subgroup1.getGroupUUID();
+ AccountGroup.UUID subgroupUuid2 = subgroup2.getGroupUUID();
+ AccountGroup.UUID subgroupUuid3 = subgroup3.getGroupUUID();
+
+ // Add two accounts.
+ addMembers(uuid, ImmutableSet.of(id1, id2));
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Add: Account %s <%s@server-id>\n", id1, id1)
+ + String.format("Add: Account %s <%s@server-id>", id2, id2));
+ AccountGroupMemberAudit expMemberAudit1 =
+ createExpMemberAudit(groupId, id1, userId, getTipTimestamp(uuid));
+ AccountGroupMemberAudit expMemberAudit2 =
+ createExpMemberAudit(groupId, id2, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(uuid))
+ .containsExactly(expMemberAudit, expMemberAudit1, expMemberAudit2)
+ .inOrder();
+
+ // Add one subgroup.
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid1));
+ assertTipCommit(uuid, String.format("Update group\n\nAdd-group: Group <%s>", subgroupUuid1));
+ AccountGroupByIdAud expGroupAudit1 =
+ createExpGroupAudit(group.getId(), subgroupUuid1, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(uuid)).containsExactly(expGroupAudit1);
+
+ // Remove one account.
+ removeMembers(uuid, ImmutableSet.of(id2));
+ assertTipCommit(
+ uuid, String.format("Update group\n\nRemove: Account %s <%s@server-id>", id2, id2));
+ expMemberAudit2.removed(userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(uuid))
+ .containsExactly(expMemberAudit, expMemberAudit1, expMemberAudit2)
+ .inOrder();
+
+ // Add two subgroups.
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid2, subgroupUuid3));
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Add-group: Group <%s>\n", subgroupUuid2)
+ + String.format("Add-group: Group <%s>", subgroupUuid3));
+ AccountGroupByIdAud expGroupAudit2 =
+ createExpGroupAudit(group.getId(), subgroupUuid2, userId, getTipTimestamp(uuid));
+ AccountGroupByIdAud expGroupAudit3 =
+ createExpGroupAudit(group.getId(), subgroupUuid3, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(uuid))
+ .containsExactly(expGroupAudit1, expGroupAudit2, expGroupAudit3)
+ .inOrder();
+
+ // Add two account, including a removed account.
+ addMembers(uuid, ImmutableSet.of(id2, id3));
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Add: Account %s <%s@server-id>\n", id2, id2)
+ + String.format("Add: Account %s <%s@server-id>", id3, id3));
+ AccountGroupMemberAudit expMemberAudit4 =
+ createExpMemberAudit(groupId, id2, userId, getTipTimestamp(uuid));
+ AccountGroupMemberAudit expMemberAudit3 =
+ createExpMemberAudit(groupId, id3, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getMembersAudit(uuid))
+ .containsExactly(
+ expMemberAudit, expMemberAudit1, expMemberAudit2, expMemberAudit4, expMemberAudit3)
+ .inOrder();
+
+ // Remove two subgroups.
+ removeSubgroups(uuid, ImmutableSet.of(subgroupUuid1, subgroupUuid3));
+ assertTipCommit(
+ uuid,
+ "Update group\n"
+ + "\n"
+ + String.format("Remove-group: Group <%s>\n", subgroupUuid1)
+ + String.format("Remove-group: Group <%s>", subgroupUuid3));
+ expGroupAudit1.removed(userId, getTipTimestamp(uuid));
+ expGroupAudit3.removed(userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(uuid))
+ .containsExactly(expGroupAudit1, expGroupAudit2, expGroupAudit3)
+ .inOrder();
+
+ // Add back one removed subgroup.
+ addSubgroups(uuid, ImmutableSet.of(subgroupUuid1));
+ AccountGroupByIdAud expGroupAudit4 =
+ createExpGroupAudit(group.getId(), subgroupUuid1, userId, getTipTimestamp(uuid));
+ assertThat(auditLogReader.getSubgroupsAudit(uuid))
+ .containsExactly(expGroupAudit1, expGroupAudit2, expGroupAudit3, expGroupAudit4)
+ .inOrder();
+ }
+
+ private InternalGroup createGroupAsUser(int next, String groupName) throws Exception {
+ return createGroup(next, groupName, userIdent, userId);
+ }
+
+ private InternalGroup createGroup(
+ int next, String groupName, PersonIdent authorIdent, Account.Id authorId) throws Exception {
+ InternalGroupCreation groupCreation =
+ InternalGroupCreation.builder()
+ .setGroupUUID(GroupUUID.make(groupName, serverIdent))
+ .setNameKey(new AccountGroup.NameKey(groupName))
+ .setId(new AccountGroup.Id(next))
+ .setCreatedOn(TimeUtil.nowTs())
+ .build();
+ InternalGroupUpdate groupUpdate =
+ authorIdent.equals(serverIdent)
+ ? InternalGroupUpdate.builder().setDescription("Groups").build()
+ : InternalGroupUpdate.builder()
+ .setDescription("Groups")
+ .setMemberModification(members -> ImmutableSet.of(authorId))
+ .build();
+
+ GroupConfig groupConfig = GroupConfig.createForNewGroup(allUsersRepo, groupCreation);
+ groupConfig.setGroupUpdate(
+ groupUpdate, AbstractGroupTest::getAccountNameEmail, AbstractGroupTest::getGroupName);
+
+ RevCommit commit = groupConfig.commit(createMetaDataUpdate(authorIdent));
+ assertCreateGroup(authorIdent, commit);
+ return groupConfig
+ .getLoadedGroup()
+ .orElseThrow(() -> new IllegalStateException("create group failed"));
+ }
+
+ private void assertCreateGroup(PersonIdent authorIdent, RevCommit commit) throws Exception {
+ if (authorIdent.equals(serverIdent)) {
+ assertServerCommit(CommitUtil.toCommitInfo(commit), "Create group");
+ } else {
+ assertCommit(
+ CommitUtil.toCommitInfo(commit),
+ String.format("Create group\n\nAdd: Account %s <%s@%s>", userId, userId, SERVER_ID),
+ getAccountName(userId),
+ getAccountEmail(userId));
+ }
+ }
+
+ private InternalGroup updateGroup(AccountGroup.UUID uuid, InternalGroupUpdate groupUpdate)
+ throws Exception {
+ GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersRepo, uuid);
+ groupConfig.setGroupUpdate(
+ groupUpdate, AbstractGroupTest::getAccountNameEmail, AbstractGroupTest::getGroupName);
+
+ groupConfig.commit(createMetaDataUpdate(userIdent));
+ return groupConfig
+ .getLoadedGroup()
+ .orElseThrow(() -> new IllegalStateException("updated group failed"));
+ }
+
+ private InternalGroup addMembers(AccountGroup.UUID groupUuid, Set<Account.Id> ids)
+ throws Exception {
+ InternalGroupUpdate update =
+ InternalGroupUpdate.builder()
+ .setMemberModification(memberIds -> Sets.union(memberIds, ids))
+ .build();
+ return updateGroup(groupUuid, update);
+ }
+
+ private InternalGroup removeMembers(AccountGroup.UUID groupUuid, Set<Account.Id> ids)
+ throws Exception {
+ InternalGroupUpdate update =
+ InternalGroupUpdate.builder()
+ .setMemberModification(memberIds -> Sets.difference(memberIds, ids))
+ .build();
+ return updateGroup(groupUuid, update);
+ }
+
+ private InternalGroup addSubgroups(AccountGroup.UUID groupUuid, Set<AccountGroup.UUID> uuids)
+ throws Exception {
+ InternalGroupUpdate update =
+ InternalGroupUpdate.builder()
+ .setSubgroupModification(memberIds -> Sets.union(memberIds, uuids))
+ .build();
+ return updateGroup(groupUuid, update);
+ }
+
+ private InternalGroup removeSubgroups(AccountGroup.UUID groupUuid, Set<AccountGroup.UUID> uuids)
+ throws Exception {
+ InternalGroupUpdate update =
+ InternalGroupUpdate.builder()
+ .setSubgroupModification(memberIds -> Sets.difference(memberIds, uuids))
+ .build();
+ return updateGroup(groupUuid, update);
+ }
+
+ private AccountGroupMemberAudit createExpMemberAudit(
+ AccountGroup.Id groupId, Account.Id id, Account.Id addedBy, Timestamp addedOn) {
+ return new AccountGroupMemberAudit(
+ new AccountGroupMemberAudit.Key(id, groupId, addedOn), addedBy);
+ }
+
+ private AccountGroupByIdAud createExpGroupAudit(
+ AccountGroup.Id groupId, AccountGroup.UUID uuid, Account.Id addedBy, Timestamp addedOn) {
+ return new AccountGroupByIdAud(new AccountGroupByIdAud.Key(groupId, uuid, addedOn), addedBy);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/group/db/BUILD b/javatests/com/google/gerrit/server/group/db/BUILD
new file mode 100644
index 0000000..65e09f6
--- /dev/null
+++ b/javatests/com/google/gerrit/server/group/db/BUILD
@@ -0,0 +1,19 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+ name = "db_tests",
+ size = "small",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/extensions/common/testing:common-test-util",
+ "//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/group/db/testing",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:truth",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ "//lib/jgit/org.eclipse.jgit.junit:junit",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
new file mode 100644
index 0000000..53bf866
--- /dev/null
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -0,0 +1,213 @@
+// Copyright (C) 2017 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.group.db;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_GROUPNAMES;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.group.db.testing.GroupTestUtil;
+import com.google.gerrit.server.update.RefUpdateUtil;
+import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.testing.TestTimeUtil;
+import java.util.Arrays;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GroupNameNotesTest extends GerritBaseTests {
+ private static final String SERVER_NAME = "Gerrit Server";
+ private static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
+ private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
+
+ private AtomicInteger idCounter;
+ private Repository repo;
+
+ @Before
+ public void setUp() {
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+ idCounter = new AtomicInteger();
+ repo = new InMemoryRepository(new DfsRepositoryDescription(AllUsersNameProvider.DEFAULT));
+ }
+
+ @After
+ public void tearDown() {
+ TestTimeUtil.useSystemTime();
+ }
+
+ @Test
+ public void updateGroupNames() throws Exception {
+ GroupReference g1 = newGroup("a");
+ GroupReference g2 = newGroup("b");
+
+ PersonIdent ident = newPersonIdent();
+ updateGroupNames(ident, g1, g2);
+
+ ImmutableList<CommitInfo> log = log();
+ assertThat(log).hasSize(1);
+ assertThat(log.get(0)).parents().isEmpty();
+ assertThat(log.get(0)).message().isEqualTo("Store 2 group names");
+ assertThat(log.get(0)).author().matches(ident);
+ assertThat(log.get(0)).committer().matches(ident);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+
+ // Updating the same set of names is a no-op.
+ String commit = log.get(0).commit;
+ updateGroupNames(newPersonIdent(), g1, g2);
+ log = log();
+ assertThat(log).hasSize(1);
+ assertThat(log.get(0)).commit().isEqualTo(commit);
+ }
+
+ @Test
+ public void updateGroupNamesOverwritesExistingNotes() throws Exception {
+ GroupReference g1 = newGroup("a");
+ GroupReference g2 = newGroup("b");
+
+ TestRepository<?> tr = new TestRepository<>(repo);
+ ObjectId k1 = getNoteKey(g1);
+ ObjectId k2 = getNoteKey(g2);
+ ObjectId k3 = GroupNameNotes.getNoteKey(new AccountGroup.NameKey("c"));
+ PersonIdent ident = newPersonIdent();
+ ObjectId origCommitId =
+ tr.branch(REFS_GROUPNAMES)
+ .commit()
+ .message("Prepopulate group name")
+ .author(ident)
+ .committer(ident)
+ .add(k1.name(), "[group]\n\tuuid = a-1\n\tname = a\nanotherKey = foo\n")
+ .add(k2.name(), "[group]\n\tuuid = a-1\n\tname = b\n")
+ .add(k3.name(), "[group]\n\tuuid = c-3\n\tname = c\n")
+ .create()
+ .copy();
+
+ ident = newPersonIdent();
+ updateGroupNames(ident, g1, g2);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+
+ ImmutableList<CommitInfo> log = log();
+ assertThat(log).hasSize(2);
+ assertThat(log.get(0)).commit().isEqualTo(origCommitId.name());
+
+ assertThat(log.get(1)).message().isEqualTo("Store 2 group names");
+ assertThat(log.get(1)).author().matches(ident);
+ assertThat(log.get(1)).committer().matches(ident);
+
+ try (RevWalk rw = new RevWalk(repo)) {
+ ObjectReader reader = rw.getObjectReader();
+ NoteMap noteMap =
+ NoteMap.read(reader, rw.parseCommit(ObjectId.fromString(log.get(1).commit)));
+ String note = new String(reader.open(noteMap.get(k1), OBJ_BLOB).getCachedBytes(), UTF_8);
+ // Old note content was overwritten.
+ assertThat(note).isEqualTo("[group]\n\tuuid = a-1\n\tname = a\n");
+ }
+ }
+
+ @Test
+ public void updateGroupNamesWithEmptyCollectionClearsAllNotes() throws Exception {
+ GroupReference g1 = newGroup("a");
+ GroupReference g2 = newGroup("b");
+
+ PersonIdent ident = newPersonIdent();
+ updateGroupNames(ident, g1, g2);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+
+ updateGroupNames(ident);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).isEmpty();
+
+ ImmutableList<CommitInfo> log = log();
+ assertThat(log).hasSize(2);
+ assertThat(log.get(1)).message().isEqualTo("Store 0 group names");
+ }
+
+ @Test
+ public void updateGroupNamesRejectsNonOneToOneGroupReferences() throws Exception {
+ assertIllegalArgument(
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name1"),
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name2"));
+ assertIllegalArgument(
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name1"),
+ new GroupReference(new AccountGroup.UUID("uuid2"), "name1"));
+ assertIllegalArgument(
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name1"),
+ new GroupReference(new AccountGroup.UUID("uuid1"), "name1"));
+ }
+
+ private GroupReference newGroup(String name) {
+ int id = idCounter.incrementAndGet();
+ return new GroupReference(new AccountGroup.UUID(name + "-" + id), name);
+ }
+
+ private static PersonIdent newPersonIdent() {
+ return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+ }
+
+ private static ObjectId getNoteKey(GroupReference g) {
+ return GroupNameNotes.getNoteKey(new AccountGroup.NameKey(g.getName()));
+ }
+
+ private void updateGroupNames(PersonIdent ident, GroupReference... groupRefs) throws Exception {
+ try (ObjectInserter inserter = repo.newObjectInserter()) {
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+ GroupNameNotes.updateGroupNames(repo, inserter, bru, Arrays.asList(groupRefs), ident);
+ inserter.flush();
+ RefUpdateUtil.executeChecked(bru, repo);
+ }
+ }
+
+ private void assertIllegalArgument(GroupReference... groupRefs) throws Exception {
+ try (ObjectInserter inserter = repo.newObjectInserter()) {
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+ PersonIdent ident = newPersonIdent();
+ try {
+ GroupNameNotes.updateGroupNames(repo, inserter, bru, Arrays.asList(groupRefs), ident);
+ assert_().fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessageThat().isEqualTo(GroupNameNotes.UNIQUE_REF_ERROR);
+ }
+ }
+ }
+
+ private ImmutableList<CommitInfo> log() throws Exception {
+ return GroupTestUtil.log(repo, REFS_GROUPNAMES);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java b/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java
index 0c30070..e2177e2 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupRebuilderTest.java
@@ -16,12 +16,12 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_GROUPNAMES;
import static com.google.gerrit.server.group.db.GroupBundle.builder;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Streams;
import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -30,57 +30,48 @@
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.CommitUtil;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.testing.GerritBaseTests;
+import com.google.gerrit.server.group.db.testing.GroupTestUtil;
+import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gerrit.testing.TestTimeUtil;
import java.sql.Timestamp;
-import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
+import java.util.stream.IntStream;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-public class GroupRebuilderTest extends GerritBaseTests {
- private static final String SERVER_NAME = "Gerrit Server";
- private static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
- private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
-
+public class GroupRebuilderTest extends AbstractGroupTest {
private AtomicInteger idCounter;
private Repository repo;
private GroupRebuilder rebuilder;
+ private GroupBundle.Factory bundleFactory;
@Before
- public void setUp() {
+ public void setUp() throws Exception {
TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
idCounter = new AtomicInteger();
- repo = new InMemoryRepository(new DfsRepositoryDescription(AllUsersNameProvider.DEFAULT));
+ repo = repoManager.createRepository(allUsersName);
rebuilder =
new GroupRebuilder(
- () -> new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ),
- new AllUsersName(AllUsersNameProvider.DEFAULT),
+ GroupRebuilderTest::newPersonIdent,
+ allUsersName,
(project, repo, batch) ->
new MetaDataUpdate(GitReferenceUpdated.DISABLED, project, repo, batch),
// Note that the expected name/email values in tests are not necessarily realistic,
// since they use these trivial name/email functions. GroupRebuilderIT checks the actual
// values.
- (id, ident) ->
- new PersonIdent(
- "Account " + id, id + "@server-id", ident.getWhen(), ident.getTimeZone()),
- id -> String.format("Account %s <%s@server-id>", id, id),
- uuid -> "Group " + uuid);
+ AbstractGroupTest::newPersonIdent,
+ AbstractGroupTest::getAccountNameEmail,
+ AbstractGroupTest::getGroupName);
+ bundleFactory =
+ new GroupBundle.Factory(new AuditLogReader(SERVER_ID, repoManager, allUsersName));
}
@After
@@ -93,12 +84,13 @@
AccountGroup g = newGroup("a");
GroupBundle b = builder().group(g).build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(1);
assertCommit(log.get(0), "Create group", SERVER_NAME, SERVER_EMAIL);
+ assertThat(logGroupNames()).isEmpty();
}
@Test
@@ -109,9 +101,9 @@
g.setVisibleToAll(true);
GroupBundle b = builder().group(g).build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(1);
assertServerCommit(log.get(0), "Create group");
@@ -127,9 +119,9 @@
.byId(byId(g, "x"), byId(g, "y"))
.build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(2);
assertServerCommit(log.get(0), "Create group");
@@ -139,8 +131,8 @@
+ "\n"
+ "Add: Account 1 <1@server-id>\n"
+ "Add: Account 2 <2@server-id>\n"
- + "Add-group: Group x\n"
- + "Add-group: Group y");
+ + "Add-group: Group <x>\n"
+ + "Add-group: Group <y>");
}
@Test
@@ -156,9 +148,9 @@
.memberAudit(addMember(g, 1, 8, t2), addAndRemoveMember(g, 2, 8, t1, 9, t3))
.build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(4);
assertServerCommit(log.get(0), "Create group");
@@ -182,9 +174,9 @@
addMember(g, 2, 8, TimeUtil.nowTs()))
.build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(4);
assertServerCommit(log.get(0), "Create group");
@@ -206,9 +198,9 @@
.memberAudit(addMember(g, 1, 8, TimeUtil.nowTs()))
.build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(3);
assertServerCommit(log.get(0), "Create group");
@@ -231,15 +223,15 @@
.byIdAudit(addById(g, "x", 8, t2), addAndRemoveById(g, "y", 8, t1, 9, t3))
.build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(4);
assertServerCommit(log.get(0), "Create group");
- assertCommit(log.get(1), "Update group\n\nAdd-group: Group y", "Account 8", "8@server-id");
- assertCommit(log.get(2), "Update group\n\nAdd-group: Group x", "Account 8", "8@server-id");
- assertCommit(log.get(3), "Update group\n\nRemove-group: Group y", "Account 9", "9@server-id");
+ assertCommit(log.get(1), "Update group\n\nAdd-group: Group <y>", "Account 8", "8@server-id");
+ assertCommit(log.get(2), "Update group\n\nAdd-group: Group <x>", "Account 8", "8@server-id");
+ assertCommit(log.get(3), "Update group\n\nRemove-group: Group <y>", "Account 9", "9@server-id");
}
@Test
@@ -252,14 +244,14 @@
.byIdAudit(addById(g, "x", 8, TimeUtil.nowTs()))
.build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(3);
assertServerCommit(log.get(0), "Create group");
- assertCommit(log.get(1), "Update group\n\nAdd-group: Group x", "Account 8", "8@server-id");
- assertServerCommit(log.get(2), "Update group\n\nAdd-group: Group y\nAdd-group: Group z");
+ assertCommit(log.get(1), "Update group\n\nAdd-group: Group <x>", "Account 8", "8@server-id");
+ assertServerCommit(log.get(2), "Update group\n\nAdd-group: Group <y>\nAdd-group: Group <z>");
}
@Test
@@ -282,9 +274,9 @@
addAndRemoveById(g, "z", user, ts, user, ts))
.build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(5);
assertServerCommit(log.get(0), "Create group");
@@ -303,12 +295,12 @@
log.get(3),
"Update group\n"
+ "\n"
- + "Add-group: Group x\n"
- + "Add-group: Group y\n"
- + "Add-group: Group z",
+ + "Add-group: Group <x>\n"
+ + "Add-group: Group <y>\n"
+ + "Add-group: Group <z>",
"Account 8",
"8@server-id");
- assertCommit(log.get(4), "Update group\n\nRemove-group: Group z", "Account 8", "8@server-id");
+ assertCommit(log.get(4), "Update group\n\nRemove-group: Group <z>", "Account 8", "8@server-id");
}
@Test
@@ -329,9 +321,9 @@
addById(g, "x", user1, ts), addById(g, "y", user2, ts), addById(g, "z", user1, ts))
.build();
- rebuilder.rebuild(repo, b);
+ rebuilder.rebuild(repo, b, null);
- assertThat(reload(g)).isEqualTo(b.toInternalGroup());
+ assertThat(reload(g)).isEqualTo(b);
ImmutableList<CommitInfo> log = log(g);
assertThat(log).hasSize(5);
assertServerCommit(log.get(0), "Create group");
@@ -342,16 +334,79 @@
"8@server-id");
assertCommit(
log.get(2),
- "Update group\n\nAdd-group: Group x\nAdd-group: Group z",
+ "Update group\n\nAdd-group: Group <x>\nAdd-group: Group <z>",
"Account 8",
"8@server-id");
assertCommit(
log.get(3), "Update group\n\nAdd: Account 2 <2@server-id>", "Account 9", "9@server-id");
- assertCommit(log.get(4), "Update group\n\nAdd-group: Group y", "Account 9", "9@server-id");
+ assertCommit(log.get(4), "Update group\n\nAdd-group: Group <y>", "Account 9", "9@server-id");
}
- private InternalGroup reload(AccountGroup g) throws Exception {
- return GroupConfig.loadForGroup(repo, g.getGroupUUID()).getLoadedGroup().get();
+ @Test
+ public void fixupCommitPostDatesAllAuditEventsEvenIfAuditEventsAreInTheFuture() throws Exception {
+ AccountGroup g = newGroup("a");
+ IntStream.range(0, 20).forEach(i -> TimeUtil.nowTs());
+ Timestamp future = TimeUtil.nowTs();
+ TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
+
+ GroupBundle b =
+ builder()
+ .group(g)
+ .byId(byId(g, "x"), byId(g, "y"), byId(g, "z"))
+ .byIdAudit(addById(g, "x", 8, future))
+ .build();
+
+ rebuilder.rebuild(repo, b, null);
+
+ assertThat(reload(g)).isEqualTo(b);
+ ImmutableList<CommitInfo> log = log(g);
+ assertThat(log).hasSize(3);
+ assertServerCommit(log.get(0), "Create group");
+ assertCommit(log.get(1), "Update group\n\nAdd-group: Group <x>", "Account 8", "8@server-id");
+ assertServerCommit(log.get(2), "Update group\n\nAdd-group: Group <y>\nAdd-group: Group <z>");
+
+ assertThat(log.stream().map(c -> c.committer.date).collect(toImmutableList()))
+ .named("%s", log)
+ .isOrdered();
+ assertThat(TimeUtil.nowTs()).isLessThan(future);
+ }
+
+ @Test
+ public void combineWithBatchGroupNameNotes() throws Exception {
+ AccountGroup g1 = newGroup("a");
+ AccountGroup g2 = newGroup("b");
+
+ GroupBundle b1 = builder().group(g1).build();
+ GroupBundle b2 = builder().group(g2).build();
+
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+
+ rebuilder.rebuild(repo, b1, bru);
+ rebuilder.rebuild(repo, b2, bru);
+ try (ObjectInserter inserter = repo.newObjectInserter()) {
+ ImmutableList<GroupReference> refs =
+ ImmutableList.of(GroupReference.forGroup(g1), GroupReference.forGroup(g2));
+ GroupNameNotes.updateGroupNames(repo, inserter, bru, refs, newPersonIdent());
+ inserter.flush();
+ }
+
+ assertThat(log(g1)).isEmpty();
+ assertThat(log(g2)).isEmpty();
+ assertThat(logGroupNames()).isEmpty();
+
+ RefUpdateUtil.executeChecked(bru, repo);
+
+ assertThat(log(g1)).hasSize(1);
+ assertThat(log(g2)).hasSize(1);
+ assertThat(logGroupNames()).hasSize(1);
+ assertThat(reload(g1)).isEqualTo(b1);
+ assertThat(reload(g2)).isEqualTo(b2);
+
+ assertThat(GroupTestUtil.readNameToUuidMap(repo)).containsExactly("a", "a-1", "b", "b-2");
+ }
+
+ private GroupBundle reload(AccountGroup g) throws Exception {
+ return bundleFactory.fromNoteDb(repo, g.getGroupUUID());
}
private AccountGroup newGroup(String name) {
@@ -414,33 +469,14 @@
}
private ImmutableList<CommitInfo> log(AccountGroup g) throws Exception {
- ImmutableList<CommitInfo> result = ImmutableList.of();
- try (RevWalk rw = new RevWalk(repo)) {
- Ref ref = repo.exactRef(RefNames.refsGroups(g.getGroupUUID()));
- if (ref != null) {
- rw.sort(RevSort.REVERSE);
- rw.setRetainBody(true);
- rw.markStart(rw.parseCommit(ref.getObjectId()));
- result = Streams.stream(rw).map(CommitUtil::toCommitInfo).collect(toImmutableList());
- }
- }
- return result;
+ return GroupTestUtil.log(repo, RefNames.refsGroups(g.getGroupUUID()));
}
- private static void assertServerCommit(CommitInfo commitInfo, String expectedMessage) {
- assertCommit(commitInfo, expectedMessage, SERVER_NAME, SERVER_EMAIL);
+ private ImmutableList<CommitInfo> logGroupNames() throws Exception {
+ return GroupTestUtil.log(repo, REFS_GROUPNAMES);
}
- private static void assertCommit(
- CommitInfo commitInfo, String expectedMessage, String expectedName, String expectedEmail) {
- assertThat(commitInfo).message().isEqualTo(expectedMessage);
- assertThat(commitInfo).author().name().isEqualTo(expectedName);
- assertThat(commitInfo).author().email().isEqualTo(expectedEmail);
-
- // Committer should always be the server, regardless of author.
- assertThat(commitInfo).committer().name().isEqualTo(SERVER_NAME);
- assertThat(commitInfo).committer().email().isEqualTo(SERVER_EMAIL);
- assertThat(commitInfo).committer().date().isEqualTo(commitInfo.author.date);
- assertThat(commitInfo).committer().tz().isEqualTo(commitInfo.author.tz);
+ private static InternalGroup removeRefState(InternalGroup group) throws Exception {
+ return group.toBuilder().setRefState(null).build();
}
}
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index 1083a36..d4ecb6d 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -30,10 +30,10 @@
@Ignore
public class FakeChangeIndex implements ChangeIndex {
- static Schema<ChangeData> V1 =
+ static final Schema<ChangeData> V1 =
new Schema<>(1, ImmutableList.<FieldDef<ChangeData, ?>>of(ChangeField.STATUS));
- static Schema<ChangeData> V2 =
+ static final Schema<ChangeData> V2 =
new Schema<>(2, ImmutableList.of(ChangeField.STATUS, ChangeField.PATH, ChangeField.UPDATED));
private static class Source implements ChangeDataSource {
diff --git a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
index b9f6411..acb33e9 100644
--- a/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
+++ b/javatests/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -77,7 +77,7 @@
assertInvalidState("project1:refs/heads/foo:");
assertThat(
- StalenessChecker.parseStates(
+ RefState.parseStates(
byteArrays(
P1 + ":refs/heads/foo:" + SHA1,
P1 + ":refs/heads/bar:" + SHA2,
@@ -91,7 +91,7 @@
private static void assertInvalidState(String state) {
try {
- StalenessChecker.parseStates(byteArrays(state));
+ RefState.parseStates(byteArrays(state));
assert_().fail("expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected.
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index c889fc6..61e62f7 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -638,7 +638,7 @@
}
/** Boiler plate code to check two byte arrays for equality */
- private class ByteArrayWrapper {
+ private static class ByteArrayWrapper {
private byte[] arr;
private ByteArrayWrapper(byte[] arr) {
@@ -652,5 +652,10 @@
}
return Arrays.equals(arr, ((ByteArrayWrapper) other).arr);
}
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(arr);
+ }
}
}
diff --git a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index bdcee40..e4b0da5 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -32,6 +32,7 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.testing.InMemoryDatabase;
import com.google.gerrit.testing.InMemoryH2Type;
@@ -115,6 +116,7 @@
bind(SystemGroupBackend.class);
install(new NotesMigration.Module());
+ install(new GroupsMigration.Module());
bind(MetricMaker.class).to(DisabledMetricMaker.class);
}
})
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index 25bb545..9745f9e 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -212,11 +212,11 @@
* Check whether there is no newer patch than the latest patch that was
* available when this change was loaded.
*
- * @return {Promise<boolean>} A promise that yields true if the latest patch
+ * @return {Promise<!Object>} A promise that yields true if the latest patch
* has been loaded, and false if a newer patch has been uploaded in the
* meantime. The promise is rejected on network error.
*/
- fetchIsLatestKnown(change, restAPI) {
+ fetchChangeUpdates(change, restAPI) {
const knownLatest = Gerrit.PatchSetBehavior.computeLatestPatchNum(
Gerrit.PatchSetBehavior.computeAllPatchSets(change));
return restAPI.getChangeDetail(change._number)
@@ -226,7 +226,11 @@
}
const actualLatest = Gerrit.PatchSetBehavior.computeLatestPatchNum(
Gerrit.PatchSetBehavior.computeAllPatchSets(detail));
- return actualLatest <= knownLatest;
+ return {
+ isLatest: actualLatest <= knownLatest,
+ newStatus: change.status !== detail.status ? detail.status : null,
+ newMessages: change.messages.length < detail.messages.length,
+ };
});
},
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index bad47b9..7116c5d 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -35,31 +35,37 @@
assert.equal(get(revisions, '3'), undefined);
});
- test('fetchIsLatestKnown on latest', done => {
+ test('fetchChangeUpdates on latest', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
+ status: 'NEW',
+ messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(knownChange);
},
};
- Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi)
- .then(isLatest => {
- assert.isTrue(isLatest);
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isFalse(result.newMessages);
done();
});
});
- test('fetchIsLatestKnown not on latest', done => {
+ test('fetchChangeUpdates not on latest', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
+ status: 'NEW',
+ messages: [],
};
const actualChange = {
revisions: {
@@ -67,15 +73,81 @@
sha2: {description: 'patch 2', _number: 2},
sha3: {description: 'patch 3', _number: 3},
},
+ status: 'NEW',
+ messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(actualChange);
},
};
- Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi)
- .then(isLatest => {
- assert.isFalse(isLatest);
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isFalse(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates new status', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'MERGED',
+ messages: [],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.equal(result.newStatus, 'MERGED');
+ assert.isFalse(result.newMessages);
+ done();
+ });
+ });
+
+ test('fetchChangeUpdates new messages', done => {
+ const knownChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [],
+ };
+ const actualChange = {
+ revisions: {
+ sha1: {description: 'patch 1', _number: 1},
+ sha2: {description: 'patch 2', _number: 2},
+ },
+ status: 'NEW',
+ messages: [{message: 'blah blah'}],
+ };
+ const mockRestApi = {
+ getChangeDetail() {
+ return Promise.resolve(actualChange);
+ },
+ };
+ Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+ .then(result => {
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isTrue(result.newMessages);
done();
});
});
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
index 9fe10bc..2d84179 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
@@ -70,10 +70,10 @@
confirm-label="Create"
on-confirm="_handleCreateGroup"
on-cancel="_handleCloseCreate">
- <div class="header">
+ <div class="header" slot="header">
Create Group
</div>
- <div class="main">
+ <div class="main" slot="main">
<gr-create-group-dialog
has-new-group-name="{{_hasNewGroupName}}"
params="[[params]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
index e132100..da20181 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
@@ -30,8 +30,8 @@
confirm-label="Delete [[_computeItemName(itemType)]]"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">[[_computeItemName(itemType)]] Deletion</div>
- <div class="main">
+ <div class="header" slot="header">[[_computeItemName(itemType)]] Deletion</div>
+ <div class="main" slot="main">
<label for="branchInput">
Do you really want to delete the following [[_computeItemName(itemType)]]?
</label>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html
index 883a802..4274749 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-commands/gr-project-commands.html
@@ -77,10 +77,10 @@
disabled="[[!_canCreate]]"
on-confirm="_handleCreateChange"
on-cancel="_handleCloseCreateChange">
- <div class="header">
+ <div class="header" slot="header">
Create Change
</div>
- <div class="main">
+ <div class="main" slot="main">
<gr-create-change-dialog
id="createNewChangeModal"
can-create="{{_canCreate}}"
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
index 85e8f83..0ed278a 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
@@ -187,10 +187,10 @@
confirm-label="Create"
on-confirm="_handleCreateItem"
on-cancel="_handleCloseCreate">
- <div class="header">
+ <div class="header" slot="header">
Create [[_computeItemName(detailType)]]
</div>
- <div class="main">
+ <div class="main" slot="main">
<gr-create-pointer-dialog
id="createNewModal"
detail-type="[[_computeItemName(detailType)]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html
index 6c45704..958d2dc 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html
@@ -81,10 +81,10 @@
confirm-label="Create"
on-confirm="_handleCreateProject"
on-cancel="_handleCloseCreate">
- <div class="header">
+ <div class="header" slot="header">
Create Project
</div>
- <div class="main">
+ <div class="main" slot="main">
<gr-create-project-dialog
has-new-project-name="{{_hasNewProjectName}}"
params="[[params]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-project/gr-project.js b/polygerrit-ui/app/elements/admin/gr-project/gr-project.js
index 0f14088..b54824b 100644
--- a/polygerrit-ui/app/elements/admin/gr-project/gr-project.js
+++ b/polygerrit-ui/app/elements/admin/gr-project/gr-project.js
@@ -165,7 +165,7 @@
_formatBooleanSelect(item) {
if (!item) { return; }
let inheritLabel = 'Inherit';
- if (item.inherited_value) {
+ if (!(item.inherited_value === undefined)) {
inheritLabel = `Inherit (${item.inherited_value})`;
}
return [
diff --git a/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html b/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html
index 4decadb..ce7fe80 100644
--- a/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-project/gr-project_test.html
@@ -166,7 +166,7 @@
});
test('_formatBooleanSelect', () => {
- let item = {inherited_value: 'true'};
+ let item = {inherited_value: true};
assert.deepEqual(element._formatBooleanSelect(item), [
{
label: 'Inherit (true)',
@@ -181,6 +181,21 @@
},
]);
+ item = {inherited_value: false};
+ assert.deepEqual(element._formatBooleanSelect(item), [
+ {
+ label: 'Inherit (false)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ }, {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
// For items without inherited values
item = {};
assert.deepEqual(element._formatBooleanSelect(item), [
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 79a9245..481cd76 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -52,11 +52,6 @@
/* px because don't have the same font size */
margin-left: 12px;
}
- gr-button[data-action-key='submit'] {
- --gr-button-background: #f67070;
- --gr-button-color: #fff;
- --gr-button-hover-background-color: #dc5151;
- }
#actionLoadingMessage {
align-items: center;
color: #777;
@@ -107,11 +102,6 @@
on-tap="_handleActionTap">[[action.label]]</gr-button>
</template>
</section>
- <gr-button
- class="reply"
- secondary
- disabled="[[replyDisabled]]"
- on-tap="_handleReplyTap">[[replyButtonLabel]]</gr-button>
<section id="secondaryActions"
hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
<template
@@ -179,10 +169,10 @@
confirm-label="Delete"
on-cancel="_handleConfirmDialogCancel"
on-confirm="_handleDeleteConfirm">
- <div class="header">
+ <div class="header" slot="header">
Delete Change
</div>
- <div class="main">
+ <div class="main" slot="main">
Do you really want to delete the change?
</div>
</gr-confirm-dialog>
@@ -192,10 +182,10 @@
confirm-label="Delete"
on-cancel="_handleConfirmDialogCancel"
on-confirm="_handleDeleteEditConfirm">
- <div class="header">
+ <div class="header" slot="header">
Delete Change Edit
</div>
- <div class="main">
+ <div class="main" slot="main">
Do you really want to delete the edit?
</div>
</gr-confirm-dialog>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index c86c251..b2a6f0d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -160,12 +160,6 @@
* @event reload-change
*/
- /**
- * Fired when the reply button is tapped.
- *
- * @event reply-tap
- */
-
/**
* Fired when an action is tapped.
*
@@ -214,8 +208,6 @@
type: Object,
value() { return {}; },
},
- replyButtonLabel: String,
- replyDisabled: Boolean,
_loading: {
type: Boolean,
@@ -729,11 +721,6 @@
this._showActionDialog(this.$.confirmRevertDialog);
},
- _handleReplyTap(e) {
- e.preventDefault();
- this.dispatchEvent(new CustomEvent('reply-tap'));
- },
-
_handleActionTap(e) {
e.preventDefault();
const el = Polymer.dom(e).localTarget;
@@ -1062,9 +1049,9 @@
this._handleResponseError(response);
};
- return this.fetchIsLatestKnown(this.change, this.$.restAPI)
- .then(isLatest => {
- if (!isLatest) {
+ return this.fetchChangeUpdates(this.change, this.$.restAPI)
+ .then(result => {
+ if (!result.isLatest) {
this.fire('show-alert', {
message: 'Cannot set label: a newer patch has been ' +
'uploaded to this change.',
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index a3932c2..62b4626 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -194,7 +194,7 @@
const buttonEls = Polymer.dom(element.root)
.querySelectorAll('gr-button');
const menuItems = element.$.moreActions.items;
- assert.equal(buttonEls.length + menuItems.length, 7);
+ assert.equal(buttonEls.length + menuItems.length, 6);
assert.isFalse(element.hidden);
done();
});
@@ -241,8 +241,8 @@
test('submit change', done => {
sandbox.stub(element.$.restAPI, 'getFromProjectLookup')
.returns(Promise.resolve('test'));
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(true); });
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: true}); });
element.change = {
revisions: {
rev1: {_number: 1},
@@ -1264,8 +1264,8 @@
let sendStub;
setup(() => {
- sandbox.stub(element, 'fetchIsLatestKnown')
- .returns(Promise.resolve(true));
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
sendStub = sandbox.stub(element.$.restAPI, 'getChangeURLAndSend')
.returns(Promise.resolve({}));
});
@@ -1293,8 +1293,8 @@
suite('failure modes', () => {
test('non-latest', () => {
- sandbox.stub(element, 'fetchIsLatestKnown')
- .returns(Promise.resolve(false));
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: false}));
const sendStub = sandbox.stub(element.$.restAPI,
'getChangeURLAndSend');
@@ -1307,8 +1307,8 @@
});
test('send fails', () => {
- sandbox.stub(element, 'fetchIsLatestKnown')
- .returns(Promise.resolve(true));
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
const sendStub = sandbox.stub(element.$.restAPI,
'getChangeURLAndSend',
(num, method, patchNum, endpoint, payload, onErr) => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index eff4345..caf2fa3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -19,6 +19,7 @@
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
<link rel="import" href="../../plugins/gr-external-style/gr-external-style.html">
<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
@@ -329,6 +330,8 @@
</span>
</section>
<gr-endpoint-decorator name="change-metadata-item">
+ <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+ <gr-endpoint-param name="revision" value="[[currentRevision]]"></gr-endpoint-param>
</gr-endpoint-decorator>
</gr-external-style>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index db6c44c..00b4c2a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -37,6 +37,8 @@
properties: {
/** @type {?} */
change: Object,
+ /** @type {?} */
+ currentRevision: Object,
commitInfo: Object,
mutable: Boolean,
/**
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 522d34e..0b88f22 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -38,6 +38,9 @@
setup(() => {
sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
+ });
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({}); },
getLoggedIn() { return Promise.resolve(false); },
@@ -615,5 +618,29 @@
});
});
});
+
+ suite('plugin endpoints', () => {
+ test('endpoint params', done => {
+ element.change = {labels: {}};
+ element.currentRevision = {};
+ let hookEl;
+ let plugin;
+ Gerrit.install(
+ p => {
+ plugin = p;
+ plugin.hook('change-metadata-item').getLastAttached().then(
+ el => hookEl = el);
+ },
+ '0.1',
+ 'http://some/plugins/url.html');
+ Gerrit._setPluginsCount(0);
+ flush(() => {
+ assert.strictEqual(hookEl.plugin, plugin);
+ assert.strictEqual(hookEl.change, element.change);
+ assert.strictEqual(hookEl.revision, element.currentRevision);
+ done();
+ });
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 4df15c1..56548de 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -15,7 +15,6 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
-
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
@@ -25,6 +24,7 @@
<link rel="import" href="../../diff/gr-diff-preferences/gr-diff-preferences.html">
<link rel="import" href="../../edit/gr-edit-constants.html">
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
+<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
@@ -87,6 +87,9 @@
.header-title .headerSubject {
font-family: var(--font-family-bold);
}
+ .replyContainer {
+ margin-bottom: 1em;
+ }
gr-change-star {
margin-right: .25em;
vertical-align: -.425em;
@@ -133,6 +136,10 @@
}
.editCommitMessage {
margin-top: 1em;
+ --gr-button: {
+ padding-left: 0;
+ padding-right: 0;
+ }
}
.changeStatuses,
.commitActions {
@@ -343,14 +350,14 @@
edit-loaded="[[_editLoaded]]"
edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]"
on-reload-change="_handleReloadChange"
- on-download-tap="_handleOpenDownloadDialog"
- on-reply-tap="_handleReplyTap"></gr-change-actions>
+ on-download-tap="_handleOpenDownloadDialog"></gr-change-actions>
</div><!-- end commit actions -->
</div><!-- end header -->
<section class="changeInfo">
<div class="changeInfo-column changeMetadata hideOnMobileOverlay">
<gr-change-metadata
change="{{_change}}"
+ revision="[[_currentRevision]]"
commit-info="[[_commitInfo]]"
server-config="[[_serverConfig]]"
mutable="[[_loggedIn]]"
@@ -365,6 +372,14 @@
<hr class="mobile">
<div id="commitAndRelated" class="hideOnMobileOverlay">
<div class="commitContainer">
+ <div class="replyContainer">
+ <gr-button
+ id="replyBtn"
+ class="reply"
+ secondary
+ disabled="[[_replyDisabled]]"
+ on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
+ </div>
<div
id="commitMessage"
class$="commitMessage [[_computeCommitClass(_commitCollapsed, _latestCommitMessage)]]">
@@ -472,6 +487,10 @@
on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list>
</section>
<gr-endpoint-decorator name="change-view-integration">
+ <gr-endpoint-param name="change" value="[[_change]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="revision" value="[[_currentRevision]]">
+ </gr-endpoint-param>
</gr-endpoint-decorator>
<gr-messages-list id="messageList"
class="hideOnMobileOverlay"
@@ -495,7 +514,6 @@
class="scrollable"
no-cancel-on-outside-click
no-cancel-on-esc-key
- on-iron-overlay-opened="_handleReplyOverlayOpen"
with-backdrop>
<gr-reply-dialog id="replyDialog"
change="{{_change}}"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index f00f386..145c1f8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -41,6 +41,14 @@
const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
+ const ReloadToastMessage = {
+ NEWER_REVISION: 'A newer patch set has been uploaded',
+ RESTORED: 'This change has been restored',
+ ABANDONED: 'This change has been abandoned',
+ MERGED: 'This change has been merged',
+ NEW_MESSAGE: 'There are new messages on this change',
+ };
+
Polymer({
is: 'gr-change-view',
@@ -158,6 +166,7 @@
type: Boolean,
value: true,
},
+ _currentRevision: Object,
_currentRevisionActions: Object,
_allPatchSets: {
type: Array,
@@ -410,7 +419,7 @@
_handleReplyTap(e) {
e.preventDefault();
- this._openReplyDialog();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
},
_handleOpenDiffPrefs() {
@@ -438,15 +447,7 @@
this.$.replyDialog.draft = quoteStr;
}
this.$.replyDialog.quote = quoteStr;
- this._openReplyDialog();
- },
-
- _handleReplyOverlayOpen(e) {
- // This is needed so that focus is not set on the reply overlay
- // when the suggestion overaly from gr-autogrow-textarea opens.
- if (e.target === this.$.replyOverlay) {
- this.$.replyDialog.focus();
- }
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
},
_handleHideBackgroundContent() {
@@ -628,7 +629,7 @@
if (!loggedIn) { return; }
if (this.viewState.showReplyDialog) {
- this._openReplyDialog();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
// TODO(kaspern@): Find a better signal for when to call center.
this.async(() => { this.$.replyOverlay.center(); }, 100);
this.async(() => { this.$.replyOverlay.center(); }, 1000);
@@ -775,7 +776,7 @@
}
e.preventDefault();
- this._openReplyDialog();
+ this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
});
},
@@ -983,6 +984,7 @@
parseInt(lineHeight.slice(0, lineHeight.length - 2), 10);
this._change = change;
+ this._currentRevision = currentRevision;
if (!this._patchRange || !this._patchRange.patchNum ||
this.patchNumEquals(this._patchRange.patchNum,
currentRevision._number)) {
@@ -1249,23 +1251,37 @@
}
this._updateCheckTimerHandle = this.async(() => {
- this.fetchIsLatestKnown(this._change, this.$.restAPI)
- .then(latest => {
- if (latest) {
- this._startUpdateCheckTimer();
- } else {
- this._cancelUpdateCheckTimer();
- this.fire('show-alert', {
- message: 'A newer patch set has been uploaded.',
- // Persist this alert.
- dismissOnNavigation: true,
- action: 'Reload',
- callback: function() {
- // Load the current change without any patch range.
- Gerrit.Nav.navigateToChange(this._change);
- }.bind(this),
- });
+ this.fetchChangeUpdates(this._change, this.$.restAPI)
+ .then(result => {
+ let toastMessage = null;
+ if (!result.isLatest) {
+ toastMessage = ReloadToastMessage.NEWER_REVISION;
+ } else if (result.newStatus === this.ChangeStatus.MERGED) {
+ toastMessage = ReloadToastMessage.MERGED;
+ } else if (result.newStatus === this.ChangeStatus.ABANDONED) {
+ toastMessage = ReloadToastMessage.ABANDONED;
+ } else if (result.newStatus === this.ChangeStatus.NEW) {
+ toastMessage = ReloadToastMessage.RESTORED;
+ } else if (result.newMessages) {
+ toastMessage = ReloadToastMessage.NEW_MESSAGE;
}
+
+ if (!toastMessage) {
+ this._startUpdateCheckTimer();
+ return;
+ }
+
+ this._cancelUpdateCheckTimer();
+ this.fire('show-alert', {
+ message: toastMessage,
+ // Persist this alert.
+ dismissOnNavigation: true,
+ action: 'Reload',
+ callback: function() {
+ // Load the current change without any patch range.
+ Gerrit.Nav.navigateToChange(this._change);
+ }.bind(this),
+ });
});
}, this._serverConfig.change.update_delay * 1000);
},
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index aeb98d5..ccdc50e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -49,6 +49,11 @@
setup(() => {
sandbox = sinon.sandbox.create();
+ stub('gr-endpoint-decorator', {
+ _import: sandbox.stub().returns(Promise.resolve()),
+ });
+ // Since _endpoints are global, must reset state.
+ Gerrit._endpoints = new GrPluginEndpoints();
navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({test: 'config'}); },
@@ -120,14 +125,20 @@
test('A toggles overlay when logged in', done => {
sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown')
- .returns(Promise.resolve(true));
+ sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
element._change = {labels: {}};
+ const openSpy = sandbox.spy(element, '_openReplyDialog');
+
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
flush(() => {
assert.isTrue(element.$.replyOverlay.opened);
element.$.replyOverlay.close();
assert.isFalse(element.$.replyOverlay.opened);
+ assert(openSpy.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.ANY),
+ '_openReplyDialog should have been passed ANY');
+ assert.equal(openSpy.callCount, 1);
done();
});
});
@@ -834,6 +845,27 @@
});
});
+ test('_openReplyDialog called with `ANY` when coming from tap event',
+ () => {
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+ element._serverConfig = {};
+ MockInteractions.tap(element.$.replyBtn);
+ assert(openStub.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.ANY),
+ '_openReplyDialog should have been passed ANY');
+ assert.equal(openStub.callCount, 1);
+ });
+
+ test('_openReplyDialog called with `BODY` when coming from message reply' +
+ 'event', () => {
+ const openStub = sandbox.stub(element, '_openReplyDialog');
+ element.$.messageList.fire('reply', {message: {message: 'text'}});
+ assert(openStub.lastCall.calledWithExactly(
+ element.$.replyDialog.FocusTarget.BODY),
+ '_openReplyDialog should have been passed BODY');
+ assert.equal(openStub.callCount, 1);
+ });
+
test('reply dialog focus can be controlled', () => {
const FocusTarget = element.$.replyDialog.FocusTarget;
const openStub = sandbox.stub(element, '_openReplyDialog');
@@ -842,11 +874,13 @@
element._handleShowReplyDialog(e);
assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
'_openReplyDialog should have been passed REVIEWERS');
+ assert.equal(openStub.callCount, 1);
e.detail.value = {ccsOnly: true};
element._handleShowReplyDialog(e);
assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS),
'_openReplyDialog should have been passed CCS');
+ assert.equal(openStub.callCount, 2);
});
test('getUrlParameter functionality', () => {
@@ -940,8 +974,8 @@
suite('reply dialog tests', () => {
setup(() => {
sandbox.stub(element.$.replyDialog, '_draftChanged');
- sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown',
- () => { return Promise.resolve(true); });
+ sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: true}); });
element._change = {labels: {}};
});
@@ -991,8 +1025,8 @@
suite('commit message expand/collapse', () => {
setup(() => {
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(false); });
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: false}); });
});
test('commitCollapseToggle hidden for short commit message', () => {
@@ -1142,29 +1176,58 @@
});
test('_startUpdateCheckTimer negative delay', () => {
- sandbox.stub(element, 'fetchIsLatestKnown');
+ sandbox.stub(element, 'fetchChangeUpdates');
element._serverConfig = {change: {update_delay: -1}};
assert.isTrue(element._startUpdateCheckTimer.called);
- assert.isFalse(element.fetchIsLatestKnown.called);
+ assert.isFalse(element.fetchChangeUpdates.called);
});
test('_startUpdateCheckTimer up-to-date', () => {
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(true); });
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: true}); });
element._serverConfig = {change: {update_delay: 12345}};
assert.isTrue(element._startUpdateCheckTimer.called);
- assert.isTrue(element.fetchIsLatestKnown.called);
+ assert.isTrue(element.fetchChangeUpdates.called);
assert.equal(element.async.lastCall.args[1], 12345 * 1000);
});
test('_startUpdateCheckTimer out-of-date shows an alert', done => {
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(false); });
- element.addEventListener('show-alert', () => {
+ sandbox.stub(element, 'fetchChangeUpdates',
+ () => { return Promise.resolve({isLatest: false}); });
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message,
+ 'A newer patch set has been uploaded');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+
+ test('_startUpdateCheckTimer new status shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({
+ isLatest: true,
+ newStatus: element.ChangeStatus.MERGED,
+ }));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message, 'This change has been merged');
+ done();
+ });
+ element._serverConfig = {change: {update_delay: 12345}};
+ });
+
+ test('_startUpdateCheckTimer new messages shows an alert', done => {
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({
+ isLatest: true,
+ newMessages: true,
+ }));
+ element.addEventListener('show-alert', e => {
+ assert.equal(e.detail.message,
+ 'There are new messages on this change');
done();
});
element._serverConfig = {change: {update_delay: 12345}};
@@ -1315,5 +1378,30 @@
assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[1], 'foo');
assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.called);
});
+
+ suite('plugin endpoints', () => {
+ test('endpoint params', done => {
+ element._change = {labels: {}};
+ element._currentRevision = {};
+ let hookEl;
+ let plugin;
+ Gerrit.install(
+ p => {
+ plugin = p;
+ plugin.hook('change-view-integration').getLastAttached().then(
+ el => hookEl = el);
+ },
+ '0.1',
+ 'http://some/plugins/url.html');
+ Gerrit._setPluginsCount(0);
+ flush(() => {
+ assert.strictEqual(hookEl.plugin, plugin);
+ assert.strictEqual(hookEl.change, element._change);
+ assert.strictEqual(hookEl.revision, element._currentRevision);
+ done();
+ });
+ });
+ });
});
+
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
index d3bf159..a18e2a2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
@@ -55,8 +55,8 @@
confirm-label="Abandon"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Abandon Change</div>
- <div class="main">
+ <div class="header" slot="header">Abandon Change</div>
+ <div class="main" slot="main">
<label for="messageInput">Abandon Message</label>
<iron-autogrow-textarea
id="messageInput"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
index 5151280..34688ad 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
@@ -62,8 +62,8 @@
confirm-label="Cherry Pick"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Cherry Pick Change to Another Branch</div>
- <div class="main">
+ <div class="header" slot="header">Cherry Pick Change to Another Branch</div>
+ <div class="main" slot="main">
<label for="branchInput">
Cherry Pick to branch
</label>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
index ec6bfb4..2e530d9 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
@@ -61,8 +61,8 @@
confirm-label="Move Change"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Move Change to Another Branch</div>
- <div class="main">
+ <div class="header" slot="header">Move Change to Another Branch</div>
+ <div class="main" slot="main">
<p class="warning">
Warning: moving a change will not change its parents.
</p>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
index 2772594..582a03c 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -52,8 +52,8 @@
confirm-label="Rebase"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Confirm rebase</div>
- <div class="main">
+ <div class="header" slot="header">Confirm rebase</div>
+ <div class="main" slot="main">
<div id="rebaseOnParent" class="rebaseOption"
hidden$="[[!_displayParentOption(rebaseOnCurrent, hasParent)]]">
<input id="rebaseOnParentInput"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
index 92e8de3..5b39547 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -50,8 +50,8 @@
confirm-label="Revert"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Revert Merged Change</div>
- <div class="main">
+ <div class="header" slot="header">Revert Merged Change</div>
+ <div class="main" slot="main">
<label for="messageInput">
Revert Commit Message
</label>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index da3a547..f46af09 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -21,6 +21,7 @@
<link rel="import" href="../../diff/gr-patch-range-select/gr-patch-range-select.html">
<link rel="import" href="../../edit/gr-edit-controls/gr-edit-controls.html">
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
+<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -100,6 +101,9 @@
gr-button.selected iron-icon {
color: var(--color-link);
}
+ gr-linked-chip {
+ --linked-chip-text-color: black;
+ }
.expanded #collapseBtn,
.openFile .fileViewActions {
align-items: center;
@@ -164,14 +168,28 @@
</span>
<span class="container descriptionContainer hideOnEdit">
<span class="separator"></span>
- <gr-editable-label
- id="descriptionLabel"
- class="descriptionLabel"
- label-text="Add patchset description"
- value="[[_computePatchSetDescription(change, patchNum)]]"
- placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
- read-only="[[_descriptionReadOnly]]"
- on-changed="_handleDescriptionChanged"></gr-editable-label>
+ <template
+ is="dom-if"
+ if="[[_patchsetDescription]]">
+ <gr-linked-chip
+ id="descriptionChip"
+ text="[[_patchsetDescription]]"
+ removable="[[!_descriptionReadOnly]]"
+ on-remove="_handleDescriptionRemoved"></gr-linked-chip>
+ </template>
+ <template
+ is="dom-if"
+ if="[[!_patchsetDescription]]">
+ <gr-editable-label
+ id="descriptionLabel"
+ uppercase
+ class="descriptionLabel"
+ label-text="Add patchset description"
+ value="[[_patchsetDescription]]"
+ placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]"
+ read-only="[[_descriptionReadOnly]]"
+ on-changed="_handleDescriptionChanged"></gr-editable-label>
+ </template>
</span>
</div>
<div class$="rightControls [[_computeExpandedClass(filesExpanded)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index e0b4d6c..123b156 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -48,6 +48,10 @@
readOnly: true,
value: 225,
},
+ _patchsetDescription: {
+ type: String,
+ value: '',
+ },
_descriptionReadOnly: {
type: Boolean,
computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
@@ -67,6 +71,10 @@
Gerrit.PatchSetBehavior,
],
+ observers: [
+ '_computePatchSetDescription(change, patchNum)',
+ ],
+
_expandAllDiffs() {
this._expanded = true;
this.fire('expand-diffs');
@@ -111,10 +119,14 @@
_computePatchSetDescription(change, patchNum) {
const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
- return (rev && rev.description) ?
+ this._patchsetDescription = (rev && rev.description) ?
rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
},
+ _handleDescriptionRemoved(e) {
+ return this._updateDescription('', e);
+ },
+
/**
* @param {!Object} revisions The revisions object keyed by revision hashes
* @param {?Object} patchSet A revision already fetched from {revisions}
@@ -131,15 +143,31 @@
_handleDescriptionChanged(e) {
const desc = e.detail.trim();
+ this._updateDescription(desc, e);
+ },
+
+ /**
+ * Update the patchset description with the rest API.
+ * @param {string} desc
+ * @param {?(Event|Node)} e
+ * @return {!Promise}
+ */
+ _updateDescription(desc, e) {
+ const target = Polymer.dom(e).rootTarget;
+ if (target) { target.disabled = true; }
const rev = this.getRevisionByPatchNum(this.change.revisions,
this.patchNum);
const sha = this._getPatchsetHash(this.change.revisions, rev);
- this.$.restAPI.setDescription(this.changeNum,
- this.patchNum, desc)
+ return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc)
.then(res => {
if (res.ok) {
+ if (target) { target.disabled = false; }
this.set(['_change', 'revisions', sha, 'description'], desc);
+ this._patchsetDescription = desc;
}
+ }).catch(err => {
+ if (target) { target.disabled = false; }
+ return;
});
},
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index d47023f..8dbb06f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -96,10 +96,9 @@
'Add patchset description');
});
- test('_handleDescriptionChanged', () => {
+ test('description editing', () => {
const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
.returns(Promise.resolve({ok: true}));
- sandbox.stub(element, '_computeDescriptionReadOnly');
element.changeNum = '42';
element.basePatchNum = 'PARENT';
@@ -120,14 +119,44 @@
element.loggedIn = true;
flushAsynchronousOperations();
- const label = element.$.descriptionLabel;
- assert.equal(label.value, 'test');
- label.editing = true;
- label._inputText = 'test2';
- label._save();
- flushAsynchronousOperations();
- assert.isTrue(putDescStub.called);
- assert.equal(putDescStub.args[0][2], 'test2');
+
+ // The element has a description, so the account chip should be visible
+ // and the description label should not exist.
+ const chip = Polymer.dom(element.root).querySelector('#descriptionChip');
+ let label = Polymer.dom(element.root).querySelector('#descriptionLabel');
+
+ assert.equal(chip.text, 'test');
+ assert.isNotOk(label);
+
+ // Simulate tapping the remove button, but call function directly so that
+ // can determine what happens after the promise is resolved.
+ return element._handleDescriptionRemoved().then(() => {
+ // The API stub should be called with an empty string for the new
+ // description.
+ assert.equal(putDescStub.lastCall.args[2], '');
+
+ flushAsynchronousOperations();
+ // The editable label should now be visible and the chip hidden.
+ label = Polymer.dom(element.root).querySelector('#descriptionLabel');
+ assert.isOk(label);
+ assert.equal(getComputedStyle(chip).display, 'none');
+ assert.notEqual(getComputedStyle(label).display, 'none');
+ assert.isFalse(label.readOnly);
+ // Edit the label to have a new value of test2, and save.
+ label.editing = true;
+ label._inputText = 'test2';
+ label._save();
+ flushAsynchronousOperations();
+ // The API stub should be called with an `test2` for the new
+ // description.
+ assert.equal(putDescStub.callCount, 2);
+ assert.equal(putDescStub.lastCall.args[2], 'test2');
+ }).then(() => {
+ flushAsynchronousOperations();
+ // The chip should be visible again, and the label hidden.
+ assert.equal(getComputedStyle(label).display, 'none');
+ assert.notEqual(getComputedStyle(chip).display, 'none');
+ });
});
test('expandAllDiffs called when expand button clicked', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index e04eeb7..e93e710 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -93,6 +93,12 @@
gr-account-chip {
display: inline;
}
+ gr-button {
+ --gr-button: {
+ padding-left: 0;
+ padding-right: 0;
+ }
+ }
.collapsed gr-comment-list,
.collapsed .replyContainer,
.collapsed .hideOnCollapsed,
@@ -159,7 +165,7 @@
no-trailing-margin
class="message hideOnCollapsed"
content="[[message.message]]"
- config="[[_commentLinks]]"></gr-formatted-text>
+ config="[[_projectConfig.commentlinks]]"></gr-formatted-text>
<div class="replyContainer" hidden$="[[!showReplyButton]]" hidden>
<gr-button link small on-tap="_handleReplyTap">Reply</gr-button>
</div>
@@ -168,7 +174,7 @@
change-num="[[changeNum]]"
patch-num="[[message._revision_number]]"
project-name="[[projectName]]"
- comment-links="[[_commentLinks]]"></gr-comment-list>
+ project-config="[[_projectConfig]]"></gr-comment-list>
</div>
</template>
<template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index d907f3b..6b4499f 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -79,11 +79,10 @@
type: String,
observer: '_projectNameChanged',
},
- _commentLinks: Object,
/**
* @type {{ commentlinks: Array }}
*/
- projectConfig: Object,
+ _projectConfig: Object,
// Computed property needed to trigger Polymer value observing.
_expanded: {
type: Object,
@@ -239,7 +238,7 @@
_projectNameChanged(name) {
this.$.restAPI.getProjectConfig(name).then(config => {
- this._commentLinks = config.commentlinks;
+ this._projectConfig = config;
});
},
});
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index 780dca2..0143196 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -76,6 +76,12 @@
clear() {
this.loading = true;
this.hidden = true;
+
+ this._relatedResponse = {changes: []};
+ this._submittedTogether = [];
+ this._conflicts = [];
+ this._cherryPicks = [];
+ this._sameTopic = [];
},
reload() {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index df4391e..c035b44 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -337,12 +337,37 @@
true);
});
- test('clear hides', () => {
+ test('clear and empties', () => {
+ const changes = [{
+ project: 'foo/bar',
+ change_id: 'Ideadbeef',
+ commit: {
+ commit: 'deadbeef',
+ parents: [{commit: 'abc123'}],
+ author: {},
+ subject: 'do that thing',
+ },
+ _change_number: 12345,
+ _revision_number: 1,
+ _current_revision_number: 1,
+ status: 'NEW',
+ }];
+ element._relatedResponse = {changes};
+ element._submittedTogether = changes;
+ element._conflicts = changes;
+ element._cherryPicks = changes;
+ element._sameTopic = changes;
+
element.loading = false;
element.hidden = false;
element.clear();
assert.isTrue(element.loading);
assert.isTrue(element.hidden);
+ assert.equal(element._relatedResponse.changes.length, 0);
+ assert.equal(element._submittedTogether.length, 0);
+ assert.equal(element._conflicts.length, 0);
+ assert.equal(element._cherryPicks.length, 0);
+ assert.equal(element._sameTopic.length, 0);
});
test('update fires', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index babd95c..22b53c8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -86,7 +86,8 @@
],
};
element.serverConfig = {note_db_enabled: true};
- sandbox.stub(element, 'fetchIsLatestKnown', () => Promise.resolve(true));
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
};
setup(() => {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index a1719da..6a41652 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -237,16 +237,14 @@
open(opt_focusTarget) {
this.knownLatestState = LatestPatchState.CHECKING;
- this.fetchIsLatestKnown(this.change, this.$.restAPI)
- .then(isUpToDate => {
- this.knownLatestState = isUpToDate ?
+ this.fetchChangeUpdates(this.change, this.$.restAPI)
+ .then(result => {
+ this.knownLatestState = result.isLatest ?
LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
});
this._focusOn(opt_focusTarget);
- if (!this.draft || !this.draft.length) {
- this.draft = this._loadStoredDraft();
- }
+ this.draft = this._loadStoredDraft();
if (this.$.restAPI.hasPendingDiffDrafts()) {
this._savingComments = true;
this.$.restAPI.awaitPendingDiffDrafts().then(() => {
@@ -510,10 +508,10 @@
},
_focusOn(section) {
- if (section === FocusTarget.ANY) {
+ // Safeguard- always want to focus on something.
+ if (!section || section === FocusTarget.ANY) {
section = this._chooseFocusTarget();
- }
- if (section === FocusTarget.BODY) {
+ } else if (section === FocusTarget.BODY) {
const textarea = this.$.textarea;
textarea.async(textarea.getNativeTextarea()
.focus.bind(textarea.getNativeTextarea()));
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 278f2c6..9c19267 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -103,8 +103,8 @@
eraseDraftCommentStub = sandbox.stub(element.$.storage,
'eraseDraftComment');
- sandbox.stub(element, 'fetchIsLatestKnown',
- () => { return Promise.resolve(true); });
+ sandbox.stub(element, 'fetchChangeUpdates')
+ .returns(Promise.resolve({isLatest: true}));
// Allow the elements created by dom-repeat to be stamped.
flushAsynchronousOperations();
@@ -415,8 +415,18 @@
assert.equal(element.draft, storedDraft);
});
+ test('gets draft from storage even when text is already present', () => {
+ const storedDraft = 'hello world';
+ getDraftCommentStub.returns({message: storedDraft});
+ element.draft = 'foo bar';
+ element.open();
+ assert.isTrue(getDraftCommentStub.called);
+ assert.equal(element.draft, storedDraft);
+ });
+
test('blank if no stored draft', () => {
getDraftCommentStub.returns(null);
+ element.draft = 'foo bar';
element.open();
assert.isTrue(getDraftCommentStub.called);
assert.equal(element.draft, '');
@@ -505,6 +515,45 @@
assert.isFalse(filter({group: cc2}));
});
+ test('_focusOn', () => {
+ sandbox.stub(element, '_chooseFocusTarget');
+ element.serverConfig = {note_db_enabled: true};
+ flushAsynchronousOperations();
+ const textareaStub = sandbox.stub(element.$.textarea, 'async');
+ const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart,
+ 'async');
+ const ccStub = sandbox.stub(element.$$('#ccs').focusStart, 'async');
+ element._focusOn();
+ assert.equal(element._chooseFocusTarget.callCount, 1);
+ assert.deepEqual(textareaStub.callCount, 0);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.ANY);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 0);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.BODY);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 1);
+ assert.deepEqual(reviewerEntryStub.callCount, 0);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.REVIEWERS);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 1);
+ assert.deepEqual(reviewerEntryStub.callCount, 1);
+ assert.deepEqual(ccStub.callCount, 0);
+
+ element._focusOn(element.FocusTarget.CCS);
+ assert.equal(element._chooseFocusTarget.callCount, 2);
+ assert.deepEqual(textareaStub.callCount, 1);
+ assert.deepEqual(reviewerEntryStub.callCount, 1);
+ assert.deepEqual(ccStub.callCount, 1);
+ });
+
test('_chooseFocusTarget', () => {
element._account = null;
assert.strictEqual(
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 9a5b5ed..76da932 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -54,6 +54,12 @@
.remove {
font-size: .9em;
}
+ gr-button {
+ --gr-button: {
+ padding-left: 0;
+ padding-right: 0;
+ }
+ }
@media screen and (max-width: 50em), screen and (min-width: 75em) {
gr-account-chip:first-of-type {
margin-top: 0;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index ba7aad3..1ba44eb 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -335,8 +335,9 @@
},
_handleForwardSlashKey(e) {
+ const keyboardEvent = this.getKeyboardEvent(e);
if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e)) { return; }
+ (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; }
e.preventDefault();
this.$.searchInput.focus();
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index 9322782..209e8c9 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -76,6 +76,10 @@
ChangeComments.prototype._patchNumEquals =
Gerrit.PatchSetBehavior.patchNumEquals;
+ ChangeComments.prototype._isMergeParent =
+ Gerrit.PatchSetBehavior.isMergeParent;
+ ChangeComments.prototype._getParentIndex =
+ Gerrit.PatchSetBehavior.getParentIndex;
/**
* Get an object mapping file paths to a boolean representing whether that
@@ -300,12 +304,11 @@
*/
ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) {
// If the base of the patch range is a parent of a merge, and the comment
- // appears on a specific parent. Then only show the comment if the parent
+ // appears on a specific parent then only show the comment if the parent
// index of the comment matches that of the range.
if (comment.parent && comment.side === PARENT) {
- return Gerrit.PatchSetBehavior.isMergeParent(range.basePatchNum) &&
- comment.parent ===
- Gerrit.PatchSetBehavior.getParentIndex(range.basePatchNum);
+ return this._isMergeParent(range.basePatchNum) &&
+ comment.parent === this._getParentIndex(range.basePatchNum);
}
// If the base of the range is the parent of the patch:
diff --git a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
index 999c883..8cefabb 100644
--- a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
+++ b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
@@ -55,8 +55,8 @@
confirm-label="Delete"
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
- <div class="header">Delete Comment</div>
- <div class="main">
+ <div class="header" slot="header">Delete Comment</div>
+ <div class="main" slot="main">
<label for="messageInput">Enter comment delete reason</label>
<iron-autogrow-textarea
id="messageInput"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 5eb0a5d..62527ca 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -333,10 +333,10 @@
confirm-label="Discard"
on-confirm="_handleConfirmDiscard"
on-cancel="_closeConfirmDiscardOverlay">
- <div class="header">
+ <div class="header" slot="header">
Discard comment
</div>
- <div class="main">
+ <div class="main" slot="main">
Are you sure you want to discard this draft comment?
</div>
</gr-confirm-dialog>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 2490509..03380e4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -251,6 +251,22 @@
};
},
+ /**
+ * The only line in which add a comment tooltip is cut off is the first
+ * line. Even if there is a collapsed section, The first visible line is
+ * in the position where the second line would have been, if not for the
+ * collapsed section, so don't need to worry about this case for
+ * positioning the tooltip.
+ */
+ _positionActionBox(actionBox, startLine, range) {
+ if (startLine > 1) {
+ actionBox.placeAbove(range);
+ return;
+ }
+ actionBox.positionBelow = true;
+ actionBox.placeBelow(range);
+ },
+
_handleSelection() {
const normalizedRange = this._getNormalizedRange();
if (!normalizedRange) {
@@ -285,17 +301,18 @@
};
actionBox.side = start.side;
if (start.line === end.line) {
- actionBox.placeAbove(domRange);
+ this._positionActionBox(actionBox, start.line, domRange);
} else if (start.node instanceof Text) {
if (start.column) {
- actionBox.placeAbove(start.node.splitText(start.column));
+ this._positionActionBox(actionBox, start.line,
+ start.node.splitText(start.column));
}
start.node.parentElement.normalize(); // Undo splitText from above.
} else if (start.node.classList.contains('content') &&
- start.node.firstChild) {
- actionBox.placeAbove(start.node.firstChild);
+ start.node.firstChild) {
+ this._positionActionBox(actionBox, start.line, start.node.firstChild);
} else {
- actionBox.placeAbove(start.node);
+ this._positionActionBox(actionBox, start.line, start.node);
}
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index b63b9a4..4bbf12b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -38,6 +38,27 @@
<table id="diffTable">
<tbody class="section both">
+ <tr class="diff-row side-by-side" left-type="both" right-type="both">
+ <td class="left lineNum" data-value="1"></td>
+ <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
+ <td class="right lineNum" data-value="1"></td>
+ <td class="content both"><div class="contentText">[1] Nam cum ad me in Cumanum salutandi causa uterque</div></td>
+ </tr>
+ </tbody>
+
+ <tbody class="section delta">
+ <tr class="diff-row side-by-side" left-type="remove" right-type="add">
+ <td class="left lineNum" data-value="2"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl>udiam, <hl>quid</hl> sit, <span class="tab-indicator" style="tab-size:8;"> </span>quod <hl>Epicurum</hl></div></td>
+ <td class="right lineNum" data-value="2"></td>
+ <!-- Next tag is formatted to eliminate zero-length text nodes. -->
+ <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab-indicator" style="tab-size:8;"> </span></hl> otiosum, <span class="tab-indicator" style="tab-size:8;"> </span> audiam, sit, quod</div></td>
+ </tr>
+ </tbody>
+
+
+ <tbody class="section both">
<tr class="diff-row side-by-side" left-type="both" right-type="both">
<td class="left lineNum" data-value="138"></td>
<td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td>
@@ -253,6 +274,7 @@
contentStubs = [];
stub('gr-selection-action-box', {
placeAbove: sandbox.stub(),
+ placeBelow: sandbox.stub(),
});
diff = element.querySelector('#diffTable');
builder = {
@@ -270,9 +292,29 @@
window.getSelection().removeAllRanges();
});
+ test('single first line', () => {
+ const content = stubContent(1, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = element.$$('gr-selection-action-box');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
+ test('multiline starting on first line', () => {
+ const startContent = stubContent(1, 'right');
+ const endContent = stubContent(2, 'right');
+ sandbox.spy(element, '_positionActionBox');
+ emulateSelection(
+ startContent.firstChild, 10, endContent.lastChild, 7);
+ const actionBox = element.$$('gr-selection-action-box');
+ assert.isTrue(actionBox.positionBelow);
+ });
+
test('single line', () => {
const content = stubContent(138, 'left');
+ sandbox.spy(element, '_positionActionBox');
emulateSelection(content.firstChild, 5, content.firstChild, 12);
+ const actionBox = element.$$('gr-selection-action-box');
assert.isTrue(element.isRangeSelected());
assert.deepEqual(getActionRange(), {
startLine: 138,
@@ -281,14 +323,18 @@
endChar: 12,
});
assert.equal(getActionSide(), 'left');
+ assert.notOk(actionBox.positionBelow);
});
test('multiline', () => {
const startContent = stubContent(119, 'right');
const endContent = stubContent(120, 'right');
+ sandbox.spy(element, '_positionActionBox');
emulateSelection(
startContent.firstChild, 10, endContent.lastChild, 7);
assert.isTrue(element.isRangeSelected());
+ const actionBox = element.$$('gr-selection-action-box');
+
assert.deepEqual(getActionRange(), {
startLine: 119,
startChar: 10,
@@ -296,6 +342,7 @@
endChar: 36,
});
assert.equal(getActionSide(), 'right');
+ assert.notOk(actionBox.positionBelow);
});
test('multiple ranges aka firefox implementation', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
index 47db5f0..3993b86 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
@@ -17,34 +17,24 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-tooltip/gr-tooltip.html">
<dom-module id="gr-selection-action-box">
<template>
<style include="shared-styles">
:host {
- --gr-arrow-size: .65em;
-
- background-color: rgba(22, 22, 22, .9);
- border-radius: 3px;
- color: #fff;
cursor: pointer;
font-family: var(--font-family);
- padding: .5em .75em;
position: absolute;
white-space: nowrap;
}
- .arrow {
- border: var(--gr-arrow-size) solid transparent;
- border-top: var(--gr-arrow-size) solid rgba(22, 22, 22, 0.9);
- height: 0;
- left: calc(50% - var(--gr-arrow-size));
- margin-top: .5em;
- position: absolute;
- width: 0;
+ #tooltip {
+ --tooltip-background-color: rgba(22, 22, 22, .9);
}
</style>
- Press <strong>c</strong> to comment.
- <div class="arrow"></div>
+ <gr-tooltip id="tooltip"
+ text="press c to comment"
+ position-below="[[positionBelow]]"></gr-tooltip>
</template>
<script src="gr-selection-action-box.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index c228235..61a6eb2 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -37,6 +37,7 @@
endChar: NaN,
},
},
+ positionBelow: Boolean,
side: {
type: String,
value: '',
@@ -58,7 +59,7 @@
placeAbove(el) {
Polymer.dom.flush();
const rect = this._getTargetBoundingRect(el);
- const boxRect = this.getBoundingClientRect();
+ const boxRect = this.$.tooltip.getBoundingClientRect();
const parentRect = this.parentElement.getBoundingClientRect();
this.style.top =
rect.top - parentRect.top - boxRect.height - 6 + 'px';
@@ -66,6 +67,17 @@
rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
},
+ placeBelow(el) {
+ Polymer.dom.flush();
+ const rect = this._getTargetBoundingRect(el);
+ const boxRect = this.$.tooltip.getBoundingClientRect();
+ const parentRect = this.parentElement.getBoundingClientRect();
+ this.style.top =
+ rect.top - parentRect.top + boxRect.height - 6 + 'px';
+ this.style.left =
+ rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px';
+ },
+
_getTargetBoundingRect(el) {
let rect;
if (el instanceof Text) {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index 8c70772..7fc634c 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -38,15 +38,17 @@
suite('gr-selection-action-box', () => {
let container;
let element;
+ let sandbox;
setup(() => {
container = fixture('basic');
element = container.querySelector('gr-selection-action-box');
- sinon.stub(element, 'fire');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(element, 'fire');
});
teardown(() => {
- element.fire.restore();
+ sandbox.restore();
});
test('ignores regular keys', () => {
@@ -65,10 +67,10 @@
setup(() => {
e = {
button: 0,
- preventDefault: sinon.stub(),
- stopPropagation: sinon.stub(),
+ preventDefault: sandbox.stub(),
+ stopPropagation: sandbox.stub(),
};
- sinon.stub(element, '_fireCreateComment');
+ sandbox.stub(element, '_fireCreateComment');
});
test('event handled if main button', () => {
@@ -107,20 +109,14 @@
setup(() => {
target = container.querySelector('.target');
- sinon.stub(container, 'getBoundingClientRect').returns(
+ sandbox.stub(container, 'getBoundingClientRect').returns(
{top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6});
- sinon.stub(element, '_getTargetBoundingRect').returns(
+ sandbox.stub(element, '_getTargetBoundingRect').returns(
{top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60});
- sinon.stub(element, 'getBoundingClientRect').returns(
+ sandbox.stub(element.$.tooltip, 'getBoundingClientRect').returns(
{width: 10, height: 10});
});
- teardown(() => {
- element.getBoundingClientRect.restore();
- container.getBoundingClientRect.restore();
- element._getTargetBoundingRect.restore();
- });
-
test('placeAbove for Element argument', () => {
element.placeAbove(target);
assert.equal(element.style.top, '25px');
@@ -133,13 +129,24 @@
assert.equal(element.style.left, '72px');
});
+ test('placeBelow for Element argument', () => {
+ element.placeBelow(target);
+ assert.equal(element.style.top, '45px');
+ assert.equal(element.style.left, '72px');
+ });
+
+ test('placeBelow for Text Node argument', () => {
+ element.placeBelow(target.firstChild);
+ assert.equal(element.style.top, '45px');
+ assert.equal(element.style.left, '72px');
+ });
+
test('uses document.createRange', () => {
- sinon.spy(document, 'createRange');
+ sandbox.spy(document, 'createRange');
element._getTargetBoundingRect.restore();
- sinon.spy(element, '_getTargetBoundingRect');
+ sandbox.spy(element, '_getTargetBoundingRect');
element.placeAbove(target.firstChild);
assert.isTrue(document.createRange.called);
- document.createRange.restore();
});
});
});
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
index 9eb4974..c769d25 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
@@ -85,8 +85,8 @@
confirm-label="Edit"
on-confirm="_handleEditConfirm"
on-cancel="_handleDialogCancel">
- <div class="header">Edit a file</div>
- <div class="main">
+ <div class="header" slot="header">Edit a file</div>
+ <div class="main" slot="main">
<gr-autocomplete
placeholder="Enter an existing or new full file path."
query="[[_query]]"
@@ -100,8 +100,8 @@
confirm-label="Delete"
on-confirm="_handleDeleteConfirm"
on-cancel="_handleDialogCancel">
- <div class="header">Delete a file</div>
- <div class="main">
+ <div class="header" slot="header">Delete a file</div>
+ <div class="main" slot="main">
<gr-autocomplete
placeholder="Enter an existing full file path."
query="[[_query]]"
@@ -115,8 +115,8 @@
confirm-label="Rename"
on-confirm="_handleRenameConfirm"
on-cancel="_handleDialogCancel">
- <div class="header">Rename a file</div>
- <div class="main">
+ <div class="header" slot="header">Rename a file</div>
+ <div class="main" slot="main">
<gr-autocomplete
placeholder="Enter an existing full file path."
query="[[_query]]"
@@ -134,8 +134,8 @@
confirm-label="Restore"
on-confirm="_handleRestoreConfirm"
on-cancel="_handleDialogCancel">
- <div class="header">Restore this file?</div>
- <div class="main">
+ <div class="header" slot="header">Restore this file?</div>
+ <div class="main" slot="main">
<input
is="iron-input"
disabled
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
index 5dbd466..55164e0 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -48,7 +48,7 @@
<span class="title">Username</span>
<span
hidden$="[[usernameMutable]]"
- class="value">[[_account.username]]</span>
+ class="value">[[_username]]</span>
<span
hidden$="[[!usernameMutable]]"
class="value">
@@ -57,7 +57,7 @@
id="usernameInput"
disabled="[[_saving]]"
on-keydown="_handleKeydown"
- bind-value="{{_account.username}}">
+ bind-value="{{_username}}">
</section>
<section id="nameSection">
<span class="title">Full name</span>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 03795f6..a698c71 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -27,7 +27,7 @@
usernameMutable: {
type: Boolean,
notify: true,
- computed: '_computeUsernameMutable(_serverConfig)',
+ computed: '_computeUsernameMutable(_serverConfig, _account.username)',
},
nameMutable: {
type: Boolean,
@@ -64,11 +64,14 @@
/** @type {?} */
_account: Object,
_serverConfig: Object,
+ _username: {
+ type: String,
+ observer: '_usernameChanged',
+ },
},
observers: [
'_nameChanged(_account.name)',
- '_usernameChanged(_account.username)',
'_statusChanged(_account.status)',
],
@@ -82,7 +85,11 @@
}));
promises.push(this.$.restAPI.getAccount().then(account => {
+ // Provide predefined value for username to trigger computation of
+ // username mutability.
+ account.username = account.username || '';
this._account = account;
+ this._username = account.username;
}));
return Promise.all(promises).then(() => {
@@ -117,7 +124,7 @@
_maybeSetUsername() {
return this._hasUsernameChange && this.usernameMutable ?
- this.$.restAPI.setAccountUsername(this._account.username) :
+ this.$.restAPI.setAccountUsername(this._username) :
Promise.resolve();
},
@@ -131,8 +138,10 @@
return nameChanged || usernameChanged || statusChanged;
},
- _computeUsernameMutable(config) {
- return config.auth.editable_account_fields.includes('USER_NAME');
+ _computeUsernameMutable(config, username) {
+ // Username may not be changed once it is set.
+ return config.auth.editable_account_fields.includes('USER_NAME') &&
+ !username;
},
_computeNameMutable(config) {
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index e46be5b..d27d153 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -123,6 +123,8 @@
test('username render (mutable)', () => {
element.set('_serverConfig',
{auth: {editable_account_fields: ['USER_NAME']}});
+ element.set('_account.username', '');
+ element.set('_username', '');
const section = element.$.usernameSection;
const displaySpan = section.querySelectorAll('.value')[0];
@@ -179,10 +181,11 @@
});
test('username', done => {
+ element.set('_account.username', '');
+ element._hasUsernameChange = false;
assert.isTrue(element.usernameMutable);
- assert.isFalse(element.hasUnsavedChanges);
- element.set('_account.username', 'new username');
+ element.set('_username', 'new username');
assert.isTrue(usernameChangedSpy.called);
assert.isFalse(statusChangedSpy.called);
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
index 543c86d..26a2470 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -24,7 +24,7 @@
},
_handleMoveUpButton(e) {
- const index = e.target.dataIndex;
+ const index = Polymer.dom(e).localTarget.dataIndex;
if (index === 0) { return; }
const row = this.menuItems[index];
const prev = this.menuItems[index - 1];
@@ -32,7 +32,7 @@
},
_handleMoveDownButton(e) {
- const index = e.target.dataIndex;
+ const index = Polymer.dom(e).localTarget.dataIndex;
if (index === this.menuItems.length - 1) { return; }
const row = this.menuItems[index];
const next = this.menuItems[index + 1];
@@ -40,7 +40,7 @@
},
_handleDeleteButton(e) {
- const index = e.target.dataIndex;
+ const index = Polymer.dom(e).localTarget.dataIndex;
this.splice('menuItems', index, 1);
},
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index f16ba6c..c70ae88 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -47,9 +47,10 @@
// Click the up/down button (according to direction) for the index'th row.
// The index of the first row is 0, corresponding to the array.
function move(element, index, direction) {
- const selector =
- 'tr:nth-child(' + (index + 1) + ') .move' + direction + 'Button';
- const button = element.$$('tbody').querySelector(selector);
+ const selector = 'tr:nth-child(' + (index + 1) + ') .move' +
+ direction + 'Button';
+ const button =
+ element.$$('tbody').querySelector(selector).$$('paper-button');
MockInteractions.tap(button);
}
@@ -141,15 +142,15 @@
['first name', 'second name', 'third name']);
// Tap the delete button for the middle item.
- MockInteractions.tap(
- element.$$('tbody').querySelector('tr:nth-child(2) .remove-button'));
+ MockInteractions.tap(element.$$('tbody')
+ .querySelector('tr:nth-child(2) .remove-button').$$('paper-button'));
assertMenuNamesEqual(element, ['first name', 'third name']);
// Delete remaining items.
for (let i = 0; i < 2; i++) {
- MockInteractions.tap(
- element.$$('tbody').querySelector('tr:first-child .remove-button'));
+ MockInteractions.tap(element.$$('tbody')
+ .querySelector('tr:first-child .remove-button').$$('paper-button'));
}
assertMenuNamesEqual(element, []);
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
index 51fa616..cc35e08 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -56,6 +56,9 @@
font-family: var(--font-family-bold);
margin-left: 1em;
text-decoration: none;
+ --gr-button: {
+ padding: 0;
+ }
}
</style>
<span class="text">[[text]]</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 6918f30..a56394e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -75,7 +75,7 @@
/* styles for raised buttons specifically*/
:host([primary]) paper-button[raised],
:host([secondary]) paper-button[raised] {
- background-color: var(--gr-button-background, --color-link);
+ background-color: var(--color-link);
color: #fff;
}
:host([primary]) paper-button[raised]:hover,
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
index 320c908..6801dea 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
@@ -55,8 +55,8 @@
}
</style>
<div class="container">
- <header><content select=".header"></content></header>
- <main><content select=".main"></content></main>
+ <header><slot name="header"></slot></header>
+ <main><slot name="main"></slot></main>
<footer>
<gr-button link on-tap="_handleCancelTap">[[cancelLabel]]</gr-button>
<gr-button link primary on-tap="_handleConfirmTap" disabled="[[disabled]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
index d30bad2..6159d22 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -68,6 +68,9 @@
opacity: .6;
pointer-events: none;
}
+ a {
+ color: var(--linked-chip-text-color);
+ }
</style>
<div class$="container [[_getBackgroundClass(transparentBackground)]]">
<a href$="[[href]]">
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index e411274..45def55 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -40,6 +40,11 @@
SEND_DIFF_DRAFT: 'sendDiffDraft',
};
+ const CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE =
+ 'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)';
+ const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i;
+
+
Polymer({
is: 'gr-rest-api-interface',
@@ -840,6 +845,7 @@
this.ListChangesOption.CHANGE_ACTIONS,
this.ListChangesOption.CURRENT_ACTIONS,
this.ListChangesOption.DOWNLOAD_COMMANDS,
+ this.ListChangesOption.MESSAGES,
this.ListChangesOption.SUBMITTABLE,
this.ListChangesOption.WEB_LINKS
);
@@ -1636,6 +1642,7 @@
},
_sendDiffDraftRequest(method, changeNum, patchNum, draft) {
+ const isCreate = !draft.id && method === 'PUT';
let endpoint = '/drafts';
if (draft.id) {
endpoint += '/' + draft.id;
@@ -1652,6 +1659,11 @@
const promise = this.getChangeURLAndSend(changeNum, method, patchNum,
endpoint, body);
this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise);
+
+ if (isCreate) {
+ return this._failForCreate200(promise);
+ }
+
return promise;
},
@@ -1996,5 +2008,34 @@
`/files/${encodedPath}/blame`, patchNum, undefined, undefined,
opt_base ? {base: 't'} : undefined);
},
+
+ /**
+ * Modify the given create draft request promise so that it fails and throws
+ * an error if the response bears HTTP status 200 instead of HTTP 201.
+ * @see Issue 7763
+ * @param {Promise} promise The original promise.
+ * @return {Promise} The modified promise.
+ */
+ _failForCreate200(promise) {
+ return promise.then(result => {
+ if (result.status === 200) {
+ // Read the response headers into an object representation.
+ const headers = Array.from(result.headers.entries())
+ .reduce((obj, [key, val]) => {
+ if (!HEADER_REPORTING_BLACKLIST.test(key)) {
+ obj[key] = val;
+ }
+ return obj;
+ }, {});
+ const err = new Error([
+ CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE,
+ JSON.stringify(headers),
+ ].join('\n'));
+ // Throw the error so that it is caught by gr-reporting.
+ throw err;
+ }
+ return result;
+ });
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 4990a9a..fb5d475 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -651,24 +651,83 @@
});
});
- test('_sendDiffDraft pending requests tracked', () => {
- const obj = element._pendingRequests;
- sandbox.stub(element, 'getChangeURLAndSend', () => mockPromise());
- assert.notOk(element.hasPendingDiffDrafts());
+ suite('draft comments', () => {
+ test('_sendDiffDraftRequest pending requests tracked', () => {
+ const obj = element._pendingRequests;
+ sandbox.stub(element, 'getChangeURLAndSend', () => mockPromise());
+ assert.notOk(element.hasPendingDiffDrafts());
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 1);
- assert.isTrue(!!element.hasPendingDiffDrafts());
+ element._sendDiffDraftRequest(null, null, null, {});
+ assert.equal(obj.sendDiffDraft.length, 1);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
- element._sendDiffDraftRequest(null, null, null, {});
- assert.equal(obj.sendDiffDraft.length, 2);
- assert.isTrue(!!element.hasPendingDiffDrafts());
+ element._sendDiffDraftRequest(null, null, null, {});
+ assert.equal(obj.sendDiffDraft.length, 2);
+ assert.isTrue(!!element.hasPendingDiffDrafts());
- for (const promise of obj.sendDiffDraft) { promise.resolve(); }
+ for (const promise of obj.sendDiffDraft) { promise.resolve(); }
- return element.awaitPendingDiffDrafts().then(() => {
- assert.equal(obj.sendDiffDraft.length, 0);
- assert.isFalse(!!element.hasPendingDiffDrafts());
+ return element.awaitPendingDiffDrafts().then(() => {
+ assert.equal(obj.sendDiffDraft.length, 0);
+ assert.isFalse(!!element.hasPendingDiffDrafts());
+ });
+ });
+
+ suite('_failForCreate200', () => {
+ test('_sendDiffDraftRequest checks for 200 on create', () => {
+ const sendPromise = Promise.resolve();
+ sandbox.stub(element, 'getChangeURLAndSend').returns(sendPromise);
+ const failStub = sandbox.stub(element, '_failForCreate200')
+ .returns(Promise.resolve());
+ return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => {
+ assert.isTrue(failStub.calledOnce);
+ assert.isTrue(failStub.calledWithExactly(sendPromise));
+ });
+ });
+
+ test('_sendDiffDraftRequest no checks for 200 on non create', () => {
+ sandbox.stub(element, 'getChangeURLAndSend')
+ .returns(Promise.resolve());
+ const failStub = sandbox.stub(element, '_failForCreate200')
+ .returns(Promise.resolve());
+ return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'})
+ .then(() => {
+ assert.isFalse(failStub.called);
+ });
+ });
+
+ test('_failForCreate200 fails on 200', done => {
+ const result = {
+ ok: true,
+ status: 200,
+ headers: {entries: () => [
+ ['Set-CoOkiE', 'secret'],
+ ['Innocuous', 'hello'],
+ ]},
+ };
+ element._failForCreate200(Promise.resolve(result)).then(() => {
+ assert.isTrue(false, 'Promise should not resolve');
+ }).catch(e => {
+ assert.isOk(e);
+ assert.include(e.message, 'Saving draft resulted in HTTP 200');
+ assert.include(e.message, 'hello');
+ assert.notInclude(e.message, 'secret');
+ done();
+ });
+ });
+
+ test('_failForCreate200 does not fail on 201', done => {
+ const result = {
+ ok: true,
+ status: 201,
+ headers: {entries: () => []},
+ };
+ element._failForCreate200(Promise.resolve(result)).then(() => {
+ done();
+ }).catch(e => {
+ assert.isTrue(false, 'Promise should not fail');
+ });
+ });
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 7f61edd..add690e 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -15,11 +15,16 @@
'use strict';
// Date cutoff is one day:
- const DRAFT_MAX_AGE = 24 * 60 * 60 * 1000;
+ const CLEANUP_MAX_AGE = 24 * 60 * 60 * 1000;
// Clean up old entries no more frequently than one day.
const CLEANUP_THROTTLE_INTERVAL = 24 * 60 * 60 * 1000;
+ const CLEANUP_PREFIXES = [
+ 'draft:',
+ 'editablecontent:',
+ ];
+
Polymer({
is: 'gr-storage',
@@ -39,7 +44,7 @@
},
getDraftComment(location) {
- this._cleanupDrafts();
+ this._cleanupItems();
return this._getObject(this._getDraftKey(location));
},
@@ -53,6 +58,16 @@
this._storage.removeItem(key);
},
+ setEditableContentItem(key, message) {
+ this._setObject(this._getEditableContentKey(key),
+ {message, updated: Date.now()});
+ },
+
+ getEditableContentItem(key) {
+ this._cleanupItems();
+ return this._getObject(this._getEditableContentKey(key));
+ },
+
getPreferences() {
return this._getObject('localPrefs');
},
@@ -74,7 +89,11 @@
return key;
},
- _cleanupDrafts() {
+ _getEditableContentKey(key) {
+ return `editablecontent:${key}`;
+ },
+
+ _cleanupItems() {
// Throttle cleanup to the throttle interval.
if (this._lastCleanup &&
Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) {
@@ -82,12 +101,16 @@
}
this._lastCleanup = Date.now();
- let draft;
+ let item;
for (const key in this._storage) {
- if (key.startsWith('draft:')) {
- draft = this._getObject(key);
- if (Date.now() - draft.updated > DRAFT_MAX_AGE) {
- this._storage.removeItem(key);
+ if (!this._storage.hasOwnProperty(key)) { continue; }
+ for (const prefix of CLEANUP_PREFIXES) {
+ if (key.startsWith(prefix)) {
+ item = this._getObject(key);
+ if (Date.now() - item.updated > CLEANUP_MAX_AGE) {
+ this._storage.removeItem(key);
+ }
+ break;
}
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index ce8ec20..b1a77c7 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -33,6 +33,7 @@
<script>
suite('gr-storage tests', () => {
let element;
+ let sandbox;
function mockStorage(opt_quotaExceeded) {
return {
@@ -48,9 +49,12 @@
setup(() => {
element = fixture('basic');
+ sandbox = sinon.sandbox.create();
element._storage = mockStorage();
});
+ teardown(() => sandbox.restore());
+
test('storing, retrieving and erasing drafts', () => {
const changeNum = 1234;
const patchNum = 5;
@@ -100,7 +104,7 @@
// Make sure that the call to cleanup doesn't get throttled.
element._lastCleanup = 0;
- const cleanupSpy = sinon.spy(element, '_cleanupDrafts');
+ const cleanupSpy = sandbox.spy(element, '_cleanupItems');
// Create a message with a timestamp that is a second behind the max age.
element._storage.setItem(key, JSON.stringify({
@@ -114,8 +118,6 @@
assert.isTrue(cleanupSpy.called);
assert.isNotOk(draft);
assert.isNotOk(element._storage.getItem(key));
-
- cleanupSpy.restore();
});
test('_getDraftKey', () => {
@@ -160,5 +162,28 @@
assert.isTrue(element._exceededQuota);
assert.isNotOk(element._storage.getItem(key));
});
+
+ test('editable content items', () => {
+ const cleanupStub = sandbox.stub(element, '_cleanupItems');
+ const key = 'testKey';
+ const computedKey = element._getEditableContentKey(key);
+ // Key correctly computed.
+ assert.equal(computedKey, 'editablecontent:testKey');
+
+ element.setEditableContentItem(key, 'my content');
+
+ // Setting the draft stores it under the expected key.
+ let item = element._storage.getItem(computedKey);
+ assert.isOk(item);
+ assert.equal(JSON.parse(item).message, 'my content');
+ assert.isOk(JSON.parse(item).updated);
+
+ // getEditableContentItem performs as expected.
+ item = element.getEditableContentItem(key);
+ assert.isOk(item);
+ assert.equal(item.message, 'my content');
+ assert.isOk(item.updated);
+ assert.isTrue(cleanupStub.called);
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
index c34bc69..1744d28 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
@@ -24,7 +24,7 @@
--gr-tooltip-arrow-size: .5em;
--gr-tooltip-arrow-center-offset: 0;
- background-color: #333;
+ background-color: var(--tooltip-background-color, #333);
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
color: #fff;
font-size: .75rem;
@@ -52,11 +52,11 @@
width: 0;
}
.arrowPositionAbove {
- border-top: var(--gr-tooltip-arrow-size) solid #333;
+ border-top: var(--gr-tooltip-arrow-size) solid var(--tooltip-background-color, #333);
bottom: -var(--gr-tooltip-arrow-size);
}
.arrowPositionBelow {
- border-bottom: var(--gr-tooltip-arrow-size) solid #333;
+ border-bottom: var(--gr-tooltip-arrow-size) solid var(--tooltip-background-color, #333);
top: -var(--gr-tooltip-arrow-size);
}
</style>
diff --git a/tools/js/npm_pack.py b/tools/js/npm_pack.py
index 9eb6e34..9e8482e 100755
--- a/tools/js/npm_pack.py
+++ b/tools/js/npm_pack.py
@@ -13,6 +13,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+"""This downloads an NPM binary, and bundles it with its dependencies.
+
+This is used to assemble a pinned version of crisper, hosted on the
+Google storage bucket ("repository=GERRIT" in WORKSPACE).
+"""
+
from __future__ import print_function
import atexit