Merge "Provide method to get unmarked text from GR-FORMATTED-TEXT"
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 775fe21..a5a4f90 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -97,6 +97,7 @@
====
+[[git_commit_settings]]
=== A sample good Gerrit commit message:
====
Add sample commit message to guidelines doc
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index 116cf61..8f5b4d7 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -43,6 +43,64 @@
. Click on *Inherit project compile output path*.
. Click on *Use module compile output path*.
+== Recommended settings
+
+=== Code style
+. Go to *File -> Settings -> Editor -> Code Style*.
+. Click on *Manage*.
+. Click on *Import*.
+. Choose `IntelliJ IDEA Code Style XML`.
+. Select the file `$(gerrit_source_code)/tools/intellij/Gerrit_Code_Style.xml`.
+. Make sure that `Google Format (Gerrit)` is chosen as *Scheme*.
+
+In addition, the EditorConfig settings (which ensure a consistent style between
+Eclipse, IntelliJ, and other editors) should be applied on top of that. Those
+settings are in the file `.editorconfig` of the Gerrit source code. IntelliJ
+will automatically pick up those settings if the EditorConfig plugin is enabled
+and configured correctly as can be verified by:
+
+. Go to *File -> Settings -> Plugins*.
+. Ensure that the EditorConfig plugin is enabled.
+. Go to *File -> Settings -> Editor -> Code Style*.
+. Ensure that *Enable EditorConfig support* is checked.
+
+NOTE: If IntelliJ notifies you later on that the EditorConfig settings override
+the code style settings, simply confirm that.
+
+=== Copyright
+Copy the folder `$(gerrit_source_code)/tools/intellij/copyright` (not just the
+contents) to `$(project_data_directory)/.idea`. If it already exists, replace
+it.
+
+=== File header
+By default, IntelliJ adds a file header containing the name of the author and
+the current date to new files. To disable that, follow these steps:
+
+. Go to *File -> Settings -> Editor -> File and Code Templates*.
+. Select the tab *Includes*.
+. Select *File Header*.
+. Remove the template code in the right editor.
+
+=== Commit message
+To simplify the creation of commit messages which are compliant with the
+<<dev-contributing#commit-message,Commit Message>> format, do the following:
+
+. Go to *File -> Settings -> Version Control*.
+. Check *Commit message right margin (columns)*.
+. Make sure that 72 is specified as value.
+. Check *Wrap when typing reaches right margin*.
+
+In addition, you should follow the instructions of
+<<dev-contributing#git_commit_settings,this section>> (if you haven't
+done so already):
+
+* Install the Git hook for the `Change-Id` line.
+* Set up the HTTP access.
+
+Setting up the HTTP access will allow you to commit changes via IntelliJ without
+specifying your credentials. The Git hook won't be noticeable during a commit
+as it's executed after the commit dialog of IntelliJ was closed.
+
== Run configurations
Run configurations can be accessed on the toolbar. To edit them or add new ones,
choose *Edit Configurations* on the drop-down list of the run configurations
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index 1a43784..f523354 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -75,8 +75,9 @@
GeneralPreferencesInfo o = gApi.accounts()
.id(user42.id.toString())
.getPreferences();
- assertPrefs(o, GeneralPreferencesInfo.defaults(), "my");
+ assertPrefs(o, GeneralPreferencesInfo.defaults(), "my", "changeTable");
assertThat(o.my).hasSize(7);
+ assertThat(o.changeTable).isEmpty();
GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults();
@@ -99,6 +100,8 @@
i.diffView = DiffView.UNIFIED_DIFF;
i.my = new ArrayList<>();
i.my.add(new MenuItem("name", "url"));
+ i.changeTable = new ArrayList<>();
+ i.changeTable.add("Status");
i.urlAliases = new HashMap<>();
i.urlAliases.put("foo", "bar");
@@ -107,6 +110,7 @@
.setPreferences(i);
assertPrefs(o, i, "my");
assertThat(o.my).hasSize(1);
+ assertThat(o.changeTable).hasSize(1);
}
@Test
@@ -125,6 +129,6 @@
assertThat(o.changesPerPage).isEqualTo(newChangesPerPage);
// assert hard-coded defaults
- assertPrefs(o, d, "my", "changesPerPage");
+ assertPrefs(o, d, "my", "changeTable", "changesPerPage");
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 37ced5f..a39f300 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.common.RawInputUtil;
@@ -648,6 +649,7 @@
}
@Test
+ @GerritConfig(name = "index.testReindexAfterUpdate", value = "false")
public void getRelatedForStaleChange() throws Exception {
RevCommit c1_1 = commitBuilder()
.add("a.txt", "1")
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index a2b93cc..7ab1753 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -110,6 +110,13 @@
public static Config defaultConfig() {
Config cfg = new Config();
cfg.setBoolean("noteDb", null, "testRebuilderWrapper", true);
+
+ // Disable async reindex-if-stale check after index update. This avoids
+ // unintentional auto-rebuilding of the change in NoteDb during the read
+ // path of the reindex-if-stale check. For the purposes of this test, we
+ // want precise control over when auto-rebuilding happens.
+ cfg.setBoolean("index", null, "testReindexAfterUpdate", false);
+
return cfg;
}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index a4c74ed..13d9ad6 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -23,6 +23,7 @@
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
@@ -363,6 +364,21 @@
ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(),
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
+ if (source.get(ChangeField.REF_STATE.getName()) != null) {
+ JsonArray refStates =
+ source.get(ChangeField.REF_STATE.getName()).getAsJsonArray();
+ cd.setRefStates(
+ Iterables.transform(
+ refStates, e -> Base64.decodeBase64(e.getAsString())));
+ }
+ if (source.get(ChangeField.REF_STATE_PATTERN.getName()) != null) {
+ JsonArray refStatePatterns = source.get(
+ ChangeField.REF_STATE_PATTERN.getName()).getAsJsonArray();
+ cd.setRefStatePatterns(
+ Iterables.transform(
+ refStatePatterns, e -> Base64.decodeBase64(e.getAsString())));
+ }
+
return cd;
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 8d82e3a..3813644 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -141,6 +141,7 @@
public Boolean muteCommonPathPrefixes;
public Boolean signedOffBy;
public List<MenuItem> my;
+ public List<String> changeTable;
public Map<String, String> urlAliases;
public EmailStrategy emailStrategy;
public DefaultBase defaultBaseForMerges;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
index 7cfb1fc..ae93a83 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
@@ -19,6 +19,7 @@
public class ProjectAccessInfo extends JavaScriptObject {
public final native boolean canAddRefs() /*-{ return this.can_add ? true : false; }-*/;
public final native boolean isOwner() /*-{ return this.is_owner ? true : false; }-*/;
+ public final native boolean configVisible() /*-{ return this.config_visible ? true : false; }-*/;
protected ProjectAccessInfo() {
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index e1cfa90..24c2da7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -72,6 +72,7 @@
public class ProjectInfoScreen extends ProjectScreen {
private boolean isOwner;
+ private boolean configVisible;
private LabeledWidgetsGrid grid;
private Panel pluginOptionsPanel;
@@ -154,6 +155,7 @@
@Override
public void onSuccess(ProjectAccessInfo result) {
isOwner = result.isOwner();
+ configVisible = result.configVisible();
enableForm();
saveProject.setVisible(isOwner);
}
@@ -625,7 +627,7 @@
actionsPanel.add(createChangeAction());
}
- if (isOwner) {
+ if (isOwner && configVisible) {
actionsPanel.add(createEditConfigAction());
}
}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 8097543..faddd6c 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -22,6 +22,8 @@
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
+import static java.util.stream.Collectors.toList;
+
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2;
@@ -120,6 +122,9 @@
private static final String DELETED_FIELD = ChangeField.DELETED.getName();
private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName();
+ private static final String REF_STATE_FIELD = ChangeField.REF_STATE.getName();
+ private static final String REF_STATE_PATTERN_FIELD =
+ ChangeField.REF_STATE_PATTERN.getName();
private static final String REVIEWEDBY_FIELD =
ChangeField.REVIEWEDBY.getName();
private static final String REVIEWER_FIELD = ChangeField.REVIEWER.getName();
@@ -472,6 +477,12 @@
ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd);
decodeSubmitRecords(doc, SUBMIT_RECORD_LENIENT_FIELD,
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
+ if (fields.contains(REF_STATE_FIELD)) {
+ decodeRefStates(doc, cd);
+ }
+ if (fields.contains(REF_STATE_PATTERN_FIELD)) {
+ decodeRefStatePatterns(doc, cd);
+ }
return cd;
}
@@ -572,6 +583,16 @@
opts, cd);
}
+ private void decodeRefStates(Multimap<String, IndexableField> doc,
+ ChangeData cd) {
+ cd.setRefStates(copyAsBytes(doc.get(REF_STATE_FIELD)));
+ }
+
+ private void decodeRefStatePatterns(Multimap<String, IndexableField> doc,
+ ChangeData cd) {
+ cd.setRefStatePatterns(copyAsBytes(doc.get(REF_STATE_PATTERN_FIELD)));
+ }
+
private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc,
String fieldName, ProtobufCodec<T> codec) {
Collection<IndexableField> fields = doc.get(fieldName);
@@ -586,4 +607,16 @@
}
return result;
}
+
+ private static List<byte[]> copyAsBytes(Collection<IndexableField> fields) {
+ return fields.stream()
+ .map(
+ f -> {
+ BytesRef ref = f.binaryValue();
+ byte[] b = new byte[ref.length];
+ System.arraycopy(ref.bytes, ref.offset, b, 0, ref.length);
+ return b;
+ })
+ .collect(toList());
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
index 0ee10f0..91b568c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -188,10 +188,10 @@
private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery(
double baseWeight) throws OrmException{
- // Get the user's last 50 changes, check approvals
+ // Get the user's last 25 changes, check approvals
try {
List<ChangeData> result = internalChangeQuery
- .setLimit(50)
+ .setLimit(25)
.setRequestedFields(ImmutableSet.of(ChangeField.REVIEWER.getName()))
.query(changeQueryBuilder.owner("self"));
Map<Account.Id, MutableDouble> suggestions = new HashMap<>();
@@ -260,7 +260,7 @@
}
List<List<ChangeData>> result = internalChangeQuery
- .setLimit(100 * predicates.size())
+ .setLimit(25)
.setRequestedFields(ImmutableSet.of())
.query(predicates);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
index 8339baf..c0d81cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
@@ -22,8 +22,12 @@
import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
+
import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.MenuItem;
import com.google.gerrit.reviewdb.client.Account;
@@ -91,7 +95,7 @@
loadSection(p.getConfig(), UserConfigSections.GENERAL, null,
new GeneralPreferencesInfo(),
updateDefaults(allUserPrefs), in);
-
+ loadChangeTableColumns(r, p, dp);
return loadMyMenusAndUrlAliases(r, p, dp);
}
}
@@ -161,6 +165,22 @@
return !Strings.isNullOrEmpty(val) ? val : defaultValue;
}
+ public GeneralPreferencesInfo loadChangeTableColumns(GeneralPreferencesInfo r,
+ VersionedAccountPreferences v, VersionedAccountPreferences d) {
+ r.changeTable = changeTable(v);
+
+ Config cfg = v.getConfig();
+ if (r.changeTable.isEmpty() && !v.isDefaults()) {
+ r.changeTable = changeTable(d);
+ }
+ return r;
+ }
+
+ private static List<String> changeTable(VersionedAccountPreferences v) {
+ return Lists.newArrayList(v.getConfig().getStringList(
+ CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
+ }
+
private static Map<String, String> urlAliases(VersionedAccountPreferences v) {
HashMap<String, String> urlAliases = new HashMap<>();
Config cfg = v.getConfig();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index b70cabd..08b7b0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -21,6 +21,8 @@
import static com.google.gerrit.server.git.UserConfigSections.KEY_TOKEN;
import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -87,6 +89,7 @@
Account.Id id = rsrc.getUser().getAccountId();
GeneralPreferencesInfo n = loader.merge(id, i);
+ n.changeTable = i.changeTable;
n.my = i.my;
n.urlAliases = i.urlAliases;
@@ -105,6 +108,7 @@
storeSection(prefs.getConfig(), UserConfigSections.GENERAL, null, i,
GeneralPreferencesInfo.defaults());
+ storeMyChangeTableColumns(prefs, i.changeTable);
storeMyMenus(prefs, i.my);
storeUrlAliases(prefs, i.urlAliases);
prefs.commit(md);
@@ -125,6 +129,16 @@
}
}
+ public static void storeMyChangeTableColumns(VersionedAccountPreferences
+ prefs, List<String> changeTable) {
+ Config cfg = prefs.getConfig();
+ if (changeTable != null) {
+ unsetSection(cfg, UserConfigSections.CHANGE_TABLE);
+ cfg.setStringList(UserConfigSections.CHANGE_TABLE, null,
+ CHANGE_TABLE_COLUMN, changeTable);
+ }
+ }
+
private static void set(Config cfg, String section, String key, String val) {
if (Strings.isNullOrEmpty(val)) {
cfg.unset(UserConfigSections.MY, section, key);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
index a09466d..bbd55f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
@@ -29,6 +29,10 @@
public static final String KEY_MATCH = "match";
public static final String KEY_TOKEN = "token";
+ /** The table column user preferences. */
+ public static final String CHANGE_TABLE = "changeTable";
+ public static final String CHANGE_TABLE_COLUMN = "column";
+
/** The edit user preferences. */
public static final String EDIT = "edit";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
index 386092d..92938cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -18,6 +18,7 @@
import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
+import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gwtorm.server.OrmException;
@@ -65,14 +66,17 @@
public static class FillArgs {
public final TrackingFooters trackingFooters;
public final boolean allowsDrafts;
+ public final AllUsersName allUsers;
@Inject
FillArgs(TrackingFooters trackingFooters,
- @GerritServerConfig Config cfg) {
+ @GerritServerConfig Config cfg,
+ AllUsersName allUsers) {
this.trackingFooters = trackingFooters;
this.allowsDrafts = cfg == null
? true
: cfg.getBoolean("change", "allowDrafts", true);
+ this.allUsers = allUsers;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java
index 533e57c..84eb3bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java
@@ -17,8 +17,11 @@
import com.google.gerrit.server.query.DataSource;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
/**
* Secondary index implementation for arbitrary documents.
@@ -91,6 +94,44 @@
throws QueryParseException;
/**
+ * Get a single document from the index.
+ *
+ * @param key document key.
+ * @param opts query options. Options that do not make sense in the context of
+ * a single document, such as start, will be ignored.
+ * @return a single document if present.
+ * @throws IOException
+ */
+ default Optional<V> get(K key, QueryOptions opts) throws IOException {
+ opts = opts.withStart(0).withLimit(2);
+ List<V> results;
+ try {
+ results = getSource(keyPredicate(key), opts).read().toList();
+ } catch (QueryParseException e) {
+ throw new IOException("Unexpected QueryParseException during get()", e);
+ } catch (OrmException e) {
+ throw new IOException(e);
+ }
+ switch (results.size()) {
+ case 0:
+ return Optional.empty();
+ case 1:
+ return Optional.of(results.get(0));
+ default:
+ throw new IOException("Multiple results found in index for key "
+ + key + ": " + results);
+ }
+ }
+
+ /**
+ * Get a predicate that looks up a single document by key.
+ *
+ * @param key document key.
+ * @return a single predicate.
+ */
+ Predicate<V> keyPredicate(K key);
+
+ /**
* Mark whether this index is up-to-date and ready to serve reads.
*
* @param ready whether the index is ready
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
index cb7b3ef..406982a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
@@ -18,9 +18,16 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.index.Index;
import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.account.AccountPredicates;
public interface AccountIndex extends Index<Account.Id, AccountState> {
public interface Factory extends
IndexDefinition.IndexFactory<Account.Id, AccountState, AccountIndex> {
}
+
+ @Override
+ default Predicate<AccountState> keyPredicate(Account.Id id) {
+ return AccountPredicates.id(id);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index 225b756..1672eea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -36,13 +36,20 @@
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.SchemaUtil;
+import com.google.gerrit.server.index.change.StalenessChecker.RefState;
+import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.notedb.RobotCommentNotes;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -945,6 +952,76 @@
return result;
}
+ /**
+ * All values of all refs that were used in the course of indexing this
+ * document.
+ * <p>
+ * Emitted as UTF-8 encoded strings of the form
+ * {@code project:ref/name:[hex sha]}.
+ */
+ public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE =
+ new FieldDef.Repeatable<ChangeData, byte[]>(
+ "ref_state", FieldType.STORED_ONLY, true) {
+ @Override
+ public Iterable<byte[]> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ List<byte[]> result = new ArrayList<>();
+ Project.NameKey project = input.change().getProject();
+
+ input.editRefs().values().forEach(
+ r -> result.add(RefState.of(r).toByteArray(project)));
+ input.starRefs().values().forEach(
+ r -> result.add(RefState.of(r.ref()).toByteArray(project)));
+
+ if (PrimaryStorage.of(input.change()) == PrimaryStorage.NOTE_DB) {
+ ChangeNotes notes = input.notes();
+ result.add(RefState.create(notes.getRefName(), notes.getMetaId())
+ .toByteArray(project));
+ notes.getRobotComments(); // Force loading robot comments.
+ RobotCommentNotes robotNotes = notes.getRobotCommentNotes();
+ result.add(
+ RefState.create(robotNotes.getRefName(), robotNotes.getMetaId())
+ .toByteArray(project));
+ input.draftRefs().values().forEach(
+ r -> result.add(RefState.of(r).toByteArray(args.allUsers)));
+ }
+
+ return result;
+ }
+ };
+
+ /**
+ * All ref wildcard patterns that were used in the course of indexing this
+ * document.
+ * <p>
+ * Emitted as UTF-8 encoded strings of the form {@code project:ref/name/*}.
+ * See {@link RefStatePattern} for the pattern format.
+ */
+ public static final FieldDef<ChangeData, Iterable<byte[]>>
+ REF_STATE_PATTERN = new FieldDef.Repeatable<ChangeData, byte[]>(
+ "ref_state_pattern", FieldType.STORED_ONLY, true) {
+ @Override
+ public Iterable<byte[]> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ Change.Id id = input.getId();
+ Project.NameKey project = input.change().getProject();
+ List<byte[]> result = new ArrayList<>(3);
+ result.add(RefStatePattern.create(
+ RefNames.REFS_USERS + "*/" + RefNames.EDIT_PREFIX + id + "/*")
+ .toByteArray(project));
+ if (PrimaryStorage.of(input.change()) == PrimaryStorage.NOTE_DB) {
+ result.add(
+ RefStatePattern.create(
+ RefNames.refsStarredChangesPrefix(id) + "*")
+ .toByteArray(args.allUsers));
+ result.add(RefStatePattern.create(
+ RefNames.refsDraftCommentsPrefix(id) + "*")
+ .toByteArray(args.allUsers));
+ }
+ return result;
+ }
+ };
+
public static final Integer NOT_REVIEWED = -1;
private static String getTopic(ChangeData input) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java
index 9545c0a..c56880f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -17,10 +17,17 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.index.Index;
import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.LegacyChangeIdPredicate;
public interface ChangeIndex extends Index<Change.Id, ChangeData> {
public interface Factory extends
IndexDefinition.IndexFactory<Change.Id, ChangeData, ChangeIndex> {
}
+
+ @Override
+ default Predicate<ChangeData> keyPredicate(Change.Id id) {
+ return new LegacyChangeIdPredicate(id);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index f881f2e..f256707 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.index.change;
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;
@@ -28,7 +29,9 @@
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.config.GerritServerConfig;
import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexExecutor;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.query.change.ChangeData;
@@ -43,6 +46,7 @@
import com.google.inject.assistedinject.AssistedInject;
import com.google.inject.util.Providers;
+import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -102,16 +106,23 @@
private final ChangeNotes.Factory changeNotesFactory;
private final ChangeData.Factory changeDataFactory;
private final ThreadLocalRequestContext context;
+ private final ListeningExecutorService batchExecutor;
private final ListeningExecutorService executor;
private final DynamicSet<ChangeIndexedListener> indexedListeners;
+ private final StalenessChecker stalenessChecker;
+ private final boolean reindexAfterIndexUpdate;
@AssistedInject
- ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
+ ChangeIndexer(
+ @GerritServerConfig Config cfg,
+ SchemaFactory<ReviewDb> schemaFactory,
NotesMigration notesMigration,
ChangeNotes.Factory changeNotesFactory,
ChangeData.Factory changeDataFactory,
ThreadLocalRequestContext context,
DynamicSet<ChangeIndexedListener> indexedListeners,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
@Assisted ListeningExecutorService executor,
@Assisted ChangeIndex index) {
this.executor = executor;
@@ -121,17 +132,23 @@
this.changeDataFactory = changeDataFactory;
this.context = context;
this.indexedListeners = indexedListeners;
+ this.stalenessChecker = stalenessChecker;
+ this.batchExecutor = batchExecutor;
+ this.reindexAfterIndexUpdate = reindexAfterIndexUpdate(cfg);
this.index = index;
this.indexes = null;
}
@AssistedInject
ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
+ @GerritServerConfig Config cfg,
NotesMigration notesMigration,
ChangeNotes.Factory changeNotesFactory,
ChangeData.Factory changeDataFactory,
ThreadLocalRequestContext context,
DynamicSet<ChangeIndexedListener> indexedListeners,
+ StalenessChecker stalenessChecker,
+ @IndexExecutor(BATCH) ListeningExecutorService batchExecutor,
@Assisted ListeningExecutorService executor,
@Assisted ChangeIndexCollection indexes) {
this.executor = executor;
@@ -141,10 +158,17 @@
this.changeDataFactory = changeDataFactory;
this.context = context;
this.indexedListeners = indexedListeners;
+ this.stalenessChecker = stalenessChecker;
+ this.batchExecutor = batchExecutor;
+ this.reindexAfterIndexUpdate = reindexAfterIndexUpdate(cfg);
this.index = null;
this.indexes = indexes;
}
+ private static boolean reindexAfterIndexUpdate(Config cfg) {
+ return cfg.getBoolean("index", null, "testReindexAfterUpdate", true);
+ }
+
/**
* Start indexing a change.
*
@@ -181,6 +205,26 @@
i.replace(cd);
}
fireChangeIndexedEvent(cd.getId().get());
+
+ // Always double-check whether the change might be stale immediately after
+ // interactively indexing it. This fixes up the case where two writers write
+ // to the primary storage in one order, and the corresponding index writes
+ // happen in the opposite order:
+ // 1. Writer A writes to primary storage.
+ // 2. Writer B writes to primary storage.
+ // 3. Writer B updates index.
+ // 4. Writer A updates index.
+ //
+ // Without the extra reindexIfStale step, A has no way of knowing that it's
+ // about to overwrite the index document with stale data. It doesn't work to
+ // have A check for staleness before attempting its index update, because
+ // B's index update might not have happened when it does the check.
+ //
+ // With the extra reindexIfStale step after (3)/(4), we are able to detect
+ // and fix the staleness. It doesn't matter which order the two
+ // reindexIfStale calls actually execute in; we are guaranteed that at least
+ // one of them will execute after the second index write, (4).
+ reindexAfterIndexUpdate(cd);
}
private void fireChangeIndexedEvent(int id) {
@@ -212,6 +256,8 @@
public void index(ReviewDb db, Change change)
throws IOException, OrmException {
index(newChangeData(db, change));
+ // See comment in #index(ChangeData).
+ reindexAfterIndexUpdate(change.getProject(), change.getId());
}
/**
@@ -223,7 +269,10 @@
*/
public void index(ReviewDb db, Project.NameKey project, Change.Id changeId)
throws IOException, OrmException {
- index(newChangeData(db, project, changeId));
+ ChangeData cd = newChangeData(db, project, changeId);
+ index(cd);
+ // See comment in #index(ChangeData).
+ reindexAfterIndexUpdate(cd);
}
/**
@@ -245,28 +294,68 @@
new DeleteTask(id).call();
}
+ /**
+ * Asynchronously check if a change 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 project the project to which the change belongs.
+ * @param id ID of the change to index.
+ * @return future for reindexing the change; returns true if the change was
+ * stale.
+ */
+ public CheckedFuture<Boolean, IOException> reindexIfStale(
+ Project.NameKey project, Change.Id id) {
+ return submit(new ReindexIfStaleTask(project, id), batchExecutor);
+ }
+
+ private void reindexAfterIndexUpdate(ChangeData cd) throws IOException {
+ try {
+ reindexAfterIndexUpdate(cd.project(), cd.getId());
+ } catch (OrmException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private void reindexAfterIndexUpdate(Project.NameKey project, Change.Id id) {
+ if (reindexAfterIndexUpdate) {
+ reindexIfStale(project, id);
+ }
+ }
+
private Collection<ChangeIndex> getWriteIndexes() {
return indexes != null
? indexes.getWriteIndexes()
: Collections.singleton(index);
}
- private CheckedFuture<?, IOException> submit(Callable<?> task) {
+ private <T> CheckedFuture<T, IOException> submit(Callable<T> task) {
+ return submit(task, executor);
+ }
+
+ private static <T> CheckedFuture<T, IOException> submit(Callable<T> task,
+ ListeningExecutorService executor) {
return Futures.makeChecked(
Futures.nonCancellationPropagating(executor.submit(task)), MAPPER);
}
- private class IndexTask implements Callable<Void> {
- private final Project.NameKey project;
- private final Change.Id id;
+ private abstract class AbstractIndexTask<T> implements Callable<T> {
+ protected final Project.NameKey project;
+ protected final Change.Id id;
- private IndexTask(Project.NameKey project, Change.Id id) {
+ protected AbstractIndexTask(Project.NameKey project, Change.Id id) {
this.project = project;
this.id = id;
}
+ protected abstract T callImpl(Provider<ReviewDb> db) throws Exception;
+
@Override
- public Void call() throws Exception {
+ public abstract String toString();
+
+ @Override
+ public final T call() throws Exception {
try {
final AtomicReference<Provider<ReviewDb>> dbRef =
Atomics.newReference();
@@ -295,10 +384,7 @@
};
RequestContext oldCtx = context.setContext(newCtx);
try {
- ChangeData cd = newChangeData(
- newCtx.getReviewDbProvider().get(), project, id);
- index(cd);
- return null;
+ return callImpl(newCtx.getReviewDbProvider());
} finally {
context.setContext(oldCtx);
Provider<ReviewDb> db = dbRef.get();
@@ -307,17 +393,31 @@
}
}
} catch (Exception e) {
- log.error(String.format("Failed to index change %d", id.get()), e);
+ log.error("Failed to execute " + this, e);
throw e;
}
}
+ }
+
+ private class IndexTask extends AbstractIndexTask<Void> {
+ private IndexTask(Project.NameKey project, Change.Id id) {
+ super(project, id);
+ }
+
+ @Override
+ public Void callImpl(Provider<ReviewDb> db) throws Exception {
+ ChangeData cd = newChangeData(db.get(), project, id);
+ index(cd);
+ return null;
+ }
@Override
public String toString() {
- return "index-change-" + id.get();
+ return "index-change-" + id;
}
}
+ // Not AbstractIndexTask as it doesn't need ReviewDb.
private class DeleteTask implements Callable<Void> {
private final Change.Id id;
@@ -339,6 +439,26 @@
}
}
+ private class ReindexIfStaleTask extends AbstractIndexTask<Boolean> {
+ private ReindexIfStaleTask(Project.NameKey project, Change.Id id) {
+ super(project, id);
+ }
+
+ @Override
+ public Boolean callImpl(Provider<ReviewDb> db) throws Exception {
+ if (!stalenessChecker.isStale(id)) {
+ return false;
+ }
+ index(newChangeData(db.get(), project, id));
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "reindex-if-stale-change-" + id;
+ }
+ }
+
// Avoid auto-rebuilding when reindexing if reading is disabled. This just
// increases contention on the meta ref from a background indexing thread
// with little benefit. The next actual write to the entity may still incur a
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 8a793e5..49bccf2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -73,12 +73,18 @@
.add(ChangeField.LABEL2)
.build();
+ @Deprecated
static final Schema<ChangeData> V35 =
schema(V34,
ChangeField.SUBMIT_RECORD,
ChangeField.STORED_SUBMIT_RECORD_LENIENT,
ChangeField.STORED_SUBMIT_RECORD_STRICT);
+ static final Schema<ChangeData> V36 =
+ schema(V35,
+ ChangeField.REF_STATE,
+ ChangeField.REF_STATE_PATTERN);
+
public static final String NAME = "changes";
public static final ChangeSchemaDefinitions INSTANCE =
new ChangeSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
new file mode 100644
index 0000000..0c3d89c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -0,0 +1,308 @@
+// Copyright (C) 2016 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.change;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.StreamSupport;
+
+@Singleton
+public class StalenessChecker {
+ private static final Logger log =
+ LoggerFactory.getLogger(StalenessChecker.class);
+
+ private final ImmutableSet<String> FIELDS = ImmutableSet.of(
+ ChangeField.CHANGE.getName(),
+ ChangeField.REF_STATE.getName(),
+ ChangeField.REF_STATE_PATTERN.getName());
+
+ private final ChangeIndexCollection indexes;
+ private final GitRepositoryManager repoManager;
+ private final IndexConfig indexConfig;
+ private final Provider<ReviewDb> db;
+
+ @Inject
+ StalenessChecker(
+ ChangeIndexCollection indexes,
+ GitRepositoryManager repoManager,
+ IndexConfig indexConfig,
+ Provider<ReviewDb> db) {
+ this.indexes = indexes;
+ this.repoManager = repoManager;
+ this.indexConfig = indexConfig;
+ this.db = db;
+ }
+
+ boolean isStale(Change.Id id) throws IOException, OrmException {
+ ChangeIndex i = indexes.getSearchIndex();
+ if (i == null) {
+ return false; // No index; caller couldn't do anything if it is stale.
+ }
+ if (!i.getSchema().hasField(ChangeField.REF_STATE)
+ || !i.getSchema().hasField(ChangeField.REF_STATE_PATTERN)) {
+ return false; // Index version not new enough for this check.
+ }
+
+ Optional<ChangeData> result = i.get(
+ id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, FIELDS));
+ if (!result.isPresent()) {
+ return true; // Not in index, but caller wants it to be.
+ }
+ ChangeData cd = result.get();
+ if (reviewDbChangeIsStale(
+ cd.change(),
+ ChangeNotes.readOneReviewDbChange(db.get(), cd.getId()))) {
+ return true;
+ }
+
+ return isStale(repoManager, id, parseStates(cd), parsePatterns(cd));
+ }
+
+ @VisibleForTesting
+ static boolean isStale(GitRepositoryManager repoManager,
+ Change.Id id,
+ SetMultimap<Project.NameKey, RefState> states,
+ Multimap<Project.NameKey, RefStatePattern> patterns) {
+ Set<Project.NameKey> projects =
+ Sets.union(states.keySet(), patterns.keySet());
+
+ for (Project.NameKey p : projects) {
+ if (isStale(repoManager, id, p, states, patterns)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @VisibleForTesting
+ static boolean reviewDbChangeIsStale(
+ Change indexChange, @Nullable Change reviewDbChange) {
+ if (reviewDbChange == null) {
+ return false; // Nothing the caller can do.
+ }
+ checkArgument(indexChange.getId().equals(reviewDbChange.getId()),
+ "mismatched change ID: %s != %s",
+ indexChange.getId(), reviewDbChange.getId());
+ if (PrimaryStorage.of(reviewDbChange) != PrimaryStorage.REVIEW_DB) {
+ return false; // Not a ReviewDb change, don't check rowVersion.
+ }
+ return reviewDbChange.getRowVersion() != indexChange.getRowVersion();
+ }
+
+ private SetMultimap<Project.NameKey, RefState> parseStates(ChangeData cd) {
+ return parseStates(cd.getRefStates());
+ }
+
+ @VisibleForTesting
+ static SetMultimap<Project.NameKey, RefState> parseStates(
+ Iterable<byte[]> states) {
+ RefState.check(states != null, null);
+ SetMultimap<Project.NameKey, RefState> result = HashMultimap.create();
+ 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;
+ }
+
+ private Multimap<Project.NameKey, RefStatePattern> parsePatterns(
+ ChangeData cd) {
+ return parsePatterns(cd.getRefStatePatterns());
+ }
+
+ public static ListMultimap<Project.NameKey, RefStatePattern> parsePatterns(
+ Iterable<byte[]> patterns) {
+ RefStatePattern.check(patterns != null, null);
+ ListMultimap<Project.NameKey, RefStatePattern> result =
+ ArrayListMultimap.create();
+ for (byte[] b : patterns) {
+ RefStatePattern.check(b != null, null);
+ String s = new String(b, UTF_8);
+ List<String> parts = Splitter.on(':').splitToList(s);
+ RefStatePattern.check(parts.size() == 2, s);
+ result.put(
+ new Project.NameKey(parts.get(0)),
+ RefStatePattern.create(parts.get(1)));
+ }
+ return result;
+ }
+
+ private static boolean isStale(GitRepositoryManager repoManager,
+ Change.Id id, Project.NameKey project,
+ SetMultimap<Project.NameKey, RefState> allStates,
+ Multimap<Project.NameKey, RefStatePattern> allPatterns) {
+ try (Repository repo = repoManager.openRepository(project)) {
+ Set<RefState> states = allStates.get(project);
+ for (RefState state : states) {
+ if (!state.match(repo)) {
+ return true;
+ }
+ }
+ for (RefStatePattern pattern : allPatterns.get(project)) {
+ if (!pattern.match(repo, states)) {
+ return true;
+ }
+ }
+ return false;
+ } catch (IOException e) {
+ log.warn(
+ String.format("error checking staleness of %s in %s", id, project),
+ e);
+ return true;
+ }
+ }
+
+ @AutoValue
+ public abstract static class RefState {
+ static RefState create(String ref, String sha) {
+ return new AutoValue_StalenessChecker_RefState(
+ ref, ObjectId.fromString(sha));
+ }
+
+ static RefState create(String ref, @Nullable ObjectId id) {
+ return new AutoValue_StalenessChecker_RefState(
+ ref, firstNonNull(id, ObjectId.zeroId()));
+ }
+
+ static RefState of(Ref ref) {
+ return new AutoValue_StalenessChecker_RefState(
+ ref.getName(), ref.getObjectId());
+ }
+
+ byte[] toByteArray(Project.NameKey project) {
+ byte[] a = (project.toString() + ':' + ref() + ':').getBytes(UTF_8);
+ byte[] b = new byte[a.length + Constants.OBJECT_ID_STRING_LENGTH];
+ System.arraycopy(a, 0, b, 0, a.length);
+ id().copyTo(b, a.length);
+ return b;
+ }
+
+ private static void check(boolean condition, String str) {
+ checkArgument(condition, "invalid RefState: %s", str);
+ }
+
+ abstract String ref();
+ abstract ObjectId id();
+
+ private boolean match(Repository repo) throws IOException {
+ Ref ref = repo.exactRef(ref());
+ ObjectId expected = ref != null ? ref.getObjectId() : ObjectId.zeroId();
+ return id().equals(expected);
+ }
+ }
+
+ /**
+ * Pattern for matching refs.
+ * <p>
+ * Similar to '*' syntax for native Git refspecs, but slightly more powerful:
+ * the pattern may contain arbitrarily many asterisks. There must be at least
+ * one '*' and the first one must immediately follow a '/'.
+ */
+ @AutoValue
+ public abstract static class RefStatePattern {
+ static RefStatePattern create(String pattern) {
+ int star = pattern.indexOf('*');
+ check(star > 0 && pattern.charAt(star - 1) == '/', pattern);
+ String prefix = pattern.substring(0, star);
+ check(Repository.isValidRefName(pattern.replace('*', 'x')), pattern);
+
+ // Quote everything except the '*'s, which become ".*".
+ String regex =
+ StreamSupport.stream(Splitter.on('*').split(pattern).spliterator(), false)
+ .map(Pattern::quote)
+ .collect(joining(".*", "^", "$"));
+ return new AutoValue_StalenessChecker_RefStatePattern(
+ pattern, prefix, Pattern.compile(regex));
+ }
+
+ byte[] toByteArray(Project.NameKey project) {
+ return (project.toString() + ':' + pattern()).getBytes(UTF_8);
+ }
+
+ private static void check(boolean condition, String str) {
+ checkArgument(condition, "invalid RefStatePattern: %s", str);
+ }
+
+ abstract String pattern();
+ abstract String prefix();
+ abstract Pattern regex();
+
+ boolean match(String refName) {
+ return regex().matcher(refName).find();
+ }
+
+ private boolean match(Repository repo, Set<RefState> expected)
+ throws IOException {
+ for (Ref r : repo.getRefDatabase().getRefs(prefix()).values()) {
+ if (!match(r.getName())) {
+ continue;
+ }
+ if (!expected.contains(RefState.of(r))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index d0b0c54..a3ea381 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -372,6 +372,10 @@
return change;
}
+ public ObjectId getMetaId() {
+ return state.metaId();
+ }
+
public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() {
if (patchSets == null) {
ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b =
@@ -510,7 +514,7 @@
return draftCommentNotes;
}
- RobotCommentNotes getRobotCommentNotes() {
+ public RobotCommentNotes getRobotCommentNotes() {
return robotCommentNotes;
}
@@ -532,7 +536,7 @@
}
@Override
- protected String getRefName() {
+ public String getRefName() {
return changeMetaRef(getChangeId());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
index f60fd2d..ae280f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -17,6 +17,7 @@
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Multimap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -43,6 +44,7 @@
private ImmutableListMultimap<RevId, RobotComment> comments;
private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap;
+ private ObjectId metaId;
@AssistedInject
RobotCommentNotes(
@@ -70,20 +72,26 @@
}
@Override
- protected String getRefName() {
+ public String getRefName() {
return RefNames.robotCommentsRef(getChangeId());
}
+ @Nullable
+ public ObjectId getMetaId() {
+ return metaId;
+ }
+
@Override
protected void onLoad(LoadHandle handle)
throws IOException, ConfigInvalidException {
- ObjectId rev = handle.id();
- if (rev == null) {
+ metaId = handle.id();
+ if (metaId == null) {
loadDefaults();
return;
}
+ metaId = metaId.copy();
- RevCommit tipCommit = handle.walk().parseCommit(rev);
+ RevCommit tipCommit = handle.walk().parseCommit(metaId);
ObjectReader reader = handle.walk().getObjectReader();
revisionNoteMap = RevisionNoteMap.parseRobotComments(args.noteUtil, reader,
NoteMap.read(reader, tipCommit));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
index b3f92ff..9a9ec5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -47,7 +47,7 @@
return Predicate.or(preds);
}
- static Predicate<AccountState> id(Account.Id accountId) {
+ public static Predicate<AccountState> id(Account.Id accountId) {
return new AccountPredicate(AccountField.ID,
AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 913cffd..01b4bc1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -89,7 +89,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -348,9 +347,9 @@
private SubmitTypeRecord submitTypeRecord;
private Boolean mergeable;
private Set<String> hashtags;
- private Set<Account.Id> editsByUser;
+ private Map<Account.Id, Ref> editsByUser;
private Set<Account.Id> reviewedBy;
- private Set<Account.Id> draftsByUser;
+ private Map<Account.Id, Ref> draftsByUser;
@Deprecated
private Set<Account.Id> starredByUser;
private ImmutableMultimap<Account.Id, String> stars;
@@ -360,6 +359,9 @@
private PersonIdent author;
private PersonIdent committer;
+ private ImmutableList<byte[]> refStates;
+ private ImmutableList<byte[]> refStatePatterns;
+
@AssistedInject
private ChangeData(
GitRepositoryManager repoManager,
@@ -1098,21 +1100,25 @@
}
public Set<Account.Id> editsByUser() throws OrmException {
+ return editRefs().keySet();
+ }
+
+ public Map<Account.Id, Ref> editRefs() throws OrmException {
if (editsByUser == null) {
if (!lazyLoad) {
- return Collections.emptySet();
+ return Collections.emptyMap();
}
Change c = change();
if (c == null) {
- return Collections.emptySet();
+ return Collections.emptyMap();
}
- editsByUser = new HashSet<>();
+ editsByUser = new HashMap<>();
Change.Id id = checkNotNull(change.getId());
try (Repository repo = repoManager.openRepository(project())) {
- for (String ref
- : repo.getRefDatabase().getRefs(RefNames.REFS_USERS).keySet()) {
- if (id.equals(Change.Id.fromEditRefPart(ref))) {
- editsByUser.add(Account.Id.fromRefPart(ref));
+ for (Map.Entry<String, Ref> e
+ : repo.getRefDatabase().getRefs(RefNames.REFS_USERS).entrySet()) {
+ if (id.equals(Change.Id.fromEditRefPart(e.getKey()))) {
+ editsByUser.put(Account.Id.fromRefPart(e.getKey()), e.getValue());
}
}
} catch (IOException e) {
@@ -1123,17 +1129,31 @@
}
public Set<Account.Id> draftsByUser() throws OrmException {
+ return draftRefs().keySet();
+ }
+
+ public Map<Account.Id, Ref> draftRefs() throws OrmException {
if (draftsByUser == null) {
if (!lazyLoad) {
- return Collections.emptySet();
+ return Collections.emptyMap();
}
Change c = change();
if (c == null) {
- return Collections.emptySet();
+ return Collections.emptyMap();
}
- draftsByUser = new HashSet<>();
- for (Comment sc : commentsUtil.draftByChange(db, notes())) {
- draftsByUser.add(sc.author.getId());
+
+ draftsByUser = new HashMap<>();
+ if (notesMigration.readChanges()) {
+ for (Ref ref : commentsUtil.getDraftRefs(notes.getChangeId())) {
+ Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+ if (account != null) {
+ draftsByUser.put(account, ref);
+ }
+ }
+ } else {
+ for (Comment sc : commentsUtil.draftByChange(db, notes())) {
+ draftsByUser.put(sc.author.getId(), null);
+ }
}
}
return draftsByUser;
@@ -1262,4 +1282,20 @@
this.deletions = deletions;
}
}
+
+ public ImmutableList<byte[]> getRefStates() {
+ return refStates;
+ }
+
+ public void setRefStates(Iterable<byte[]> refStates) {
+ this.refStates = ImmutableList.copyOf(refStates);
+ }
+
+ public ImmutableList<byte[]> getRefStatePatterns() {
+ return refStatePatterns;
+ }
+
+ public void setRefStatePatterns(Iterable<byte[]> refStatePatterns) {
+ this.refStatePatterns = ImmutableList.copyOf(refStatePatterns);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index 425eb00..f7f98d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -22,7 +22,7 @@
public class LegacyChangeIdPredicate extends ChangeIndexPredicate {
private final Change.Id id;
- LegacyChangeIdPredicate(Change.Id id) {
+ public LegacyChangeIdPredicate(Change.Id id) {
super(LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
this.id = id;
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
new file mode 100644
index 0000000..cdd1e07
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -0,0 +1,359 @@
+// Copyright (C) 2016 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.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.server.index.change.StalenessChecker.isStale;
+import static com.google.gerrit.testutil.TestChanges.newChange;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.change.StalenessChecker.RefState;
+import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
+import com.google.gerrit.server.notedb.NoteDbChangeState;
+import com.google.gerrit.testutil.GerritBaseTests;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.stream.Stream;
+
+public class StalenessCheckerTest extends GerritBaseTests {
+ private static final String SHA1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+ private static final String SHA2 = "badc0feebadc0feebadc0feebadc0feebadc0fee";
+
+ private static final Project.NameKey P1 = new Project.NameKey("project1");
+ private static final Project.NameKey P2 = new Project.NameKey("project2");
+
+ private static final Change.Id C = new Change.Id(1234);
+
+ private static final ProtobufCodec<Change> CHANGE_CODEC =
+ CodecFactory.encoder(Change.class);
+
+ private GitRepositoryManager repoManager;
+ private Repository r1;
+ private Repository r2;
+ private TestRepository<Repository> tr1;
+ private TestRepository<Repository> tr2;
+
+ @Before
+ public void setUp() throws Exception {
+ repoManager = new InMemoryRepositoryManager();
+ r1 = repoManager.createRepository(P1);
+ tr1 = new TestRepository<>(r1);
+ r2 = repoManager.createRepository(P2);
+ tr2 = new TestRepository<>(r2);
+ }
+
+ @Test
+ public void parseStates() {
+ assertInvalidState(null);
+ assertInvalidState("");
+ assertInvalidState("project1:refs/heads/foo");
+ assertInvalidState("project1:refs/heads/foo:notasha");
+ assertInvalidState("project1:refs/heads/foo:");
+
+ assertThat(
+ StalenessChecker.parseStates(
+ byteArrays(
+ P1 + ":refs/heads/foo:" + SHA1,
+ P1 + ":refs/heads/bar:" + SHA2,
+ P2 + ":refs/heads/baz:" + SHA1)))
+ .isEqualTo(
+ ImmutableSetMultimap.of(
+ P1, RefState.create("refs/heads/foo", SHA1),
+ P1, RefState.create("refs/heads/bar", SHA2),
+ P2, RefState.create("refs/heads/baz", SHA1)));
+ }
+
+ private static void assertInvalidState(String state) {
+ try {
+ StalenessChecker.parseStates(byteArrays(state));
+ assert_().fail("expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void refStateToByteArray() {
+ assertThat(
+ new String(
+ RefState.create("refs/heads/foo", ObjectId.fromString(SHA1))
+ .toByteArray(P1),
+ UTF_8))
+ .isEqualTo(P1 + ":refs/heads/foo:" + SHA1);
+ assertThat(
+ new String(
+ RefState.create("refs/heads/foo", (ObjectId) null)
+ .toByteArray(P1),
+ UTF_8))
+ .isEqualTo(P1 + ":refs/heads/foo:" + ObjectId.zeroId().name());
+ }
+
+ @Test
+ public void parsePatterns() {
+ assertInvalidPattern(null);
+ assertInvalidPattern("");
+ assertInvalidPattern("project:");
+ assertInvalidPattern("project:refs/heads/foo");
+ assertInvalidPattern("project:refs/he*ds/bar");
+ assertInvalidPattern("project:refs/(he)*ds/bar");
+ assertInvalidPattern("project:invalidrefname");
+
+ ListMultimap<Project.NameKey, RefStatePattern> r =
+ StalenessChecker.parsePatterns(
+ byteArrays(
+ P1 + ":refs/heads/*",
+ P2 + ":refs/heads/foo/*/bar",
+ P2 + ":refs/heads/foo/*-baz/*/quux"));
+
+ assertThat(r.keySet()).containsExactly(P1, P2);
+ RefStatePattern p = r.get(P1).get(0);
+ assertThat(p.pattern()).isEqualTo("refs/heads/*");
+ assertThat(p.prefix()).isEqualTo("refs/heads/");
+ assertThat(p.regex().pattern()).isEqualTo("^\\Qrefs/heads/\\E.*\\Q\\E$");
+ assertThat(p.match("refs/heads/foo")).isTrue();
+ assertThat(p.match("xrefs/heads/foo")).isFalse();
+ assertThat(p.match("refs/tags/foo")).isFalse();
+
+ p = r.get(P2).get(0);
+ assertThat(p.pattern()).isEqualTo("refs/heads/foo/*/bar");
+ assertThat(p.prefix()).isEqualTo("refs/heads/foo/");
+ assertThat(p.regex().pattern())
+ .isEqualTo("^\\Qrefs/heads/foo/\\E.*\\Q/bar\\E$");
+ assertThat(p.match("refs/heads/foo//bar")).isTrue();
+ assertThat(p.match("refs/heads/foo/x/bar")).isTrue();
+ assertThat(p.match("refs/heads/foo/x/y/bar")).isTrue();
+ assertThat(p.match("refs/heads/foo/x/baz")).isFalse();
+
+ p = r.get(P2).get(1);
+ assertThat(p.pattern()).isEqualTo("refs/heads/foo/*-baz/*/quux");
+ assertThat(p.prefix()).isEqualTo("refs/heads/foo/");
+ assertThat(p.regex().pattern())
+ .isEqualTo("^\\Qrefs/heads/foo/\\E.*\\Q-baz/\\E.*\\Q/quux\\E$");
+ assertThat(p.match("refs/heads/foo/-baz//quux")).isTrue();
+ assertThat(p.match("refs/heads/foo/x-baz/x/quux")).isTrue();
+ assertThat(p.match("refs/heads/foo/x/y-baz/x/y/quux")).isTrue();
+ assertThat(p.match("refs/heads/foo/x-baz/x/y")).isFalse();
+ }
+
+ @Test
+ public void refStatePatternToByteArray() {
+ assertThat(
+ new String(RefStatePattern.create("refs/*").toByteArray(P1), UTF_8))
+ .isEqualTo(P1 + ":refs/*");
+ }
+
+ private static void assertInvalidPattern(String state) {
+ try {
+ StalenessChecker.parsePatterns(byteArrays(state));
+ assert_().fail("expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ @Test
+ public void isStaleRefStatesOnly() throws Exception {
+ String ref1 = "refs/heads/foo";
+ ObjectId id1 = tr1.update(ref1, tr1.commit().message("commit 1"));
+ String ref2 = "refs/heads/bar";
+ ObjectId id2 = tr2.update(ref2, tr2.commit().message("commit 2"));
+
+ // Not stale.
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name()),
+ P2, RefState.create(ref2, id2.name())),
+ ImmutableMultimap.of()))
+ .isFalse();
+
+ // Wrong ref value.
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, SHA1),
+ P2, RefState.create(ref2, id2.name())),
+ ImmutableMultimap.of()))
+ .isTrue();
+
+ // Swapped repos.
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id2.name()),
+ P2, RefState.create(ref2, id1.name())),
+ ImmutableMultimap.of()))
+ .isTrue();
+
+ // Two refs in same repo, not stale.
+ String ref3 = "refs/heads/baz";
+ ObjectId id3 = tr1.update(ref3, tr1.commit().message("commit 3"));
+ tr1.update(ref3, id3);
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name()),
+ P1, RefState.create(ref3, id3.name())),
+ ImmutableMultimap.of()))
+ .isFalse();
+
+ // Ignore ref not mentioned.
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name())),
+ ImmutableMultimap.of()))
+ .isFalse();
+
+ // One ref wrong.
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name()),
+ P1, RefState.create(ref3, SHA1)),
+ ImmutableMultimap.of()))
+ .isTrue();
+ }
+
+ @Test
+ public void isStaleWithRefStatePatterns() throws Exception {
+ String ref1 = "refs/heads/foo";
+ ObjectId id1 = tr1.update(ref1, tr1.commit().message("commit 1"));
+
+ // ref1 is only ref matching pattern.
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name())),
+ ImmutableMultimap.of(
+ P1, RefStatePattern.create("refs/heads/*"))))
+ .isFalse();
+
+ // Now ref2 matches pattern, so stale unless ref2 is present in state map.
+ String ref2 = "refs/heads/bar";
+ ObjectId id2 = tr1.update(ref2, tr1.commit().message("commit 2"));
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name())),
+ ImmutableMultimap.of(
+ P1, RefStatePattern.create("refs/heads/*"))))
+ .isTrue();
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name()),
+ P1, RefState.create(ref2, id2.name())),
+ ImmutableMultimap.of(
+ P1, RefStatePattern.create("refs/heads/*"))))
+ .isFalse();
+ }
+
+ @Test
+ public void isStaleWithNonPrefixPattern() throws Exception {
+ String ref1 = "refs/heads/foo";
+ ObjectId id1 = tr1.update(ref1, tr1.commit().message("commit 1"));
+ tr1.update("refs/heads/bar", tr1.commit().message("commit 2"));
+
+ // ref1 is only ref matching pattern.
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name())),
+ ImmutableMultimap.of(
+ P1, RefStatePattern.create("refs/*/foo"))))
+ .isFalse();
+
+ // Now ref2 matches pattern, so stale unless ref2 is present in state map.
+ String ref3 = "refs/other/foo";
+ ObjectId id3 = tr1.update(ref3, tr1.commit().message("commit 3"));
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name())),
+ ImmutableMultimap.of(
+ P1, RefStatePattern.create("refs/*/foo"))))
+ .isTrue();
+ assertThat(
+ isStale(
+ repoManager, C,
+ ImmutableSetMultimap.of(
+ P1, RefState.create(ref1, id1.name()),
+ P1, RefState.create(ref3, id3.name())),
+ ImmutableMultimap.of(
+ P1, RefStatePattern.create("refs/*/foo"))))
+ .isFalse();
+ }
+
+ @Test
+ public void reviewDbChangeIsStale() throws Exception {
+ Change indexChange = newChange(P1, new Account.Id(1));
+ indexChange.setNoteDbState(SHA1);
+
+ assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, null))
+ .isFalse();
+
+ Change noteDbPrimary = clone(indexChange);
+ noteDbPrimary.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
+ assertThat(
+ StalenessChecker.reviewDbChangeIsStale(indexChange, noteDbPrimary))
+ .isFalse();
+
+ assertThat(
+ StalenessChecker.reviewDbChangeIsStale(
+ indexChange, clone(indexChange)))
+ .isFalse();
+
+ // Can't easily change row version to check true case.
+ }
+
+ private static Iterable<byte[]> byteArrays(String... strs) {
+ return Stream.of(strs).map(s -> s != null ? s.getBytes(UTF_8) : null)
+ .collect(toList());
+ }
+
+ private static Change clone(Change change) {
+ return CHANGE_CODEC.decode(CHANGE_CODEC.encodeToByteArray(change));
+ }
+
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 5899e11..d22e7a8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -52,6 +52,7 @@
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.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -1611,6 +1612,32 @@
cd.currentApprovals();
}
+ @Test
+ public void reindexIfStale() throws Exception {
+ Account.Id user = createAccount("user");
+ Project.NameKey project = new Project.NameKey("repo");
+ TestRepository<Repo> repo = createProject(project.get());
+ Change change = insert(repo, newChange(repo));
+ PatchSet ps = db.patchSets().get(change.currentPatchSetId());
+
+ requestContext.setContext(newRequestContext(user));
+ assertThat(changeEditModifier.createEdit(change, ps))
+ .isEqualTo(RefUpdate.Result.NEW);
+ assertQuery("has:edit", change);
+ assertThat(indexer.reindexIfStale(project, change.getId()).get()).isFalse();
+
+ // Delete edit ref behind index's back.
+ RefUpdate ru = repo.getRepository().updateRef(
+ RefNames.refsEdit(user, change.getId(), ps.getId()));
+ ru.setForceUpdate(true);
+ assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+
+ // Index is stale.
+ assertQuery("has:edit", change);
+ assertThat(indexer.reindexIfStale(project, change.getId()).get()).isTrue();
+ assertQuery("has:edit");
+ }
+
protected ChangeInserter newChange(TestRepository<Repo> repo)
throws Exception {
return newChange(repo, null, null, null, null);
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index 1103c03..eca40a6 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -78,6 +78,10 @@
'gr-diff gr-syntax gr-syntax-selector-class': true,
};
+ var CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
+ var JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
+ var GLOBAL_LT_PATTERN = /</g;
+
Polymer({
is: 'gr-syntax-layer',
@@ -301,6 +305,7 @@
var result;
if (this._baseLanguage && baseLine !== undefined) {
+ baseLine = this._workaround(this._baseLanguage, baseLine);
result = this._hljs.highlight(this._baseLanguage, baseLine, true,
state.baseContext);
this.push('_baseRanges', this._rangesFromString(result.value));
@@ -308,6 +313,7 @@
}
if (this._revisionLanguage && revisionLine !== undefined) {
+ revisionLine = this._workaround(this._revisionLanguage, revisionLine);
result = this._hljs.highlight(this._revisionLanguage, revisionLine,
true, state.revisionContext);
this.push('_revisionRanges', this._rangesFromString(result.value));
@@ -316,6 +322,50 @@
},
/**
+ * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained
+ * cases before sending them into HLJS so that they parse correctly.
+ *
+ * Important notes:
+ * * These tests should be as constrained as possible to avoid interfering
+ * with code it shouldn't AND to avoid executing regexes as much as
+ * possible.
+ * * These tests should document the issue clearly enough that the test can
+ * be condidently removed when the issue is solved in HLJS.
+ * * These tests should rewrite the line of code to have the same number of
+ * characters. This method rewrites the string that gets parsed, but NOT
+ * the string that gets displayed and highlighted. Thus, the positions
+ * must be consistent.
+ *
+ * @param {!string} language The name of the HLJS language plugin in use.
+ * @param {!string} line The line of code to potentially rewrite.
+ * @return {string} A potentially-rewritten line of code.
+ */
+ _workaround: function(language, line) {
+ /**
+ * Prevent confusing < and << operators for the start of a meta string by
+ * converting them to a different operator.
+ * {@see Issue 4864}
+ * {@see https://github.com/isagalaev/highlight.js/issues/1341}
+ */
+ if (language === 'cpp' && CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) {
+ return line.replace(GLOBAL_LT_PATTERN, '|');
+ }
+
+ /**
+ * Prevent confusing the closing paren of a parameterized Java annotation
+ * being applied to a formal argument as the closing paren of the argument
+ * list. Rewrite the parens as spaces.
+ * {@see Issue 4776}
+ * {@see https://github.com/isagalaev/highlight.js/issues/1324}
+ */
+ if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) {
+ return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 ');
+ }
+
+ return line;
+ },
+
+ /**
* Tells whether the state has exhausted its current section.
* @param {!Object} state
* @return {boolean}
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index aa37f1a..01e9325 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -404,5 +404,41 @@
state = {sectionIndex: 3, lineIndex: 4};
assert.isTrue(element._isSectionDone(state));
});
+
+ test('workaround CPP LT directive', function() {
+ // Does nothing to regular line.
+ var line = 'int main(int argc, char** argv) { return 0; }';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Does nothing to include directive.
+ line = '#include <stdio>';
+ assert.equal(element._workaround('cpp', line), line);
+
+ // Converts left-shift operator in #define.
+ line = '#define GiB (1ull << 30)';
+ var expected = '#define GiB (1ull || 30)';
+ assert.equal(element._workaround('cpp', line), expected);
+
+ // Converts less-than operator in #if.
+ line = ' #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)';
+ expected = ' #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)';
+ assert.equal(element._workaround('cpp', line), expected);
+ });
+
+ test('workaround Java param-annotation', function() {
+ // Does nothing to regular line.
+ var line = 'public static void foo(int bar) { }';
+ assert.equal(element._workaround('java', line), line);
+
+ // Does nothing to regular annotation.
+ var line = 'public static void foo(@Nullable int bar) { }';
+ assert.equal(element._workaround('java', line), line);
+
+ // Converts parameterized annotation.
+ line = 'public static void foo(@SuppressWarnings("unused") int bar) { }';
+ var expected = 'public static void foo(@SuppressWarnings "unused" ' +
+ ' int bar) { }';
+ assert.equal(element._workaround('java', line), expected);
+ });
});
</script>
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 164bb2d..8ca0e72 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,6 @@
}
:host([disabled]) {
cursor: default;
- pointer-events: none;
}
:host([loading]),
:host([loading][disabled]) {
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 01d2585..7e91b0e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -29,6 +29,11 @@
},
},
+ listeners: {
+ 'tap': '_handleAction',
+ 'click': '_handleAction',
+ },
+
behaviors: [
Gerrit.KeyboardShortcutBehavior,
Gerrit.TooltipBehavior,
@@ -43,6 +48,13 @@
'space enter': '_handleCommitKey',
},
+ _handleAction: function(e) {
+ if (this.disabled) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ }
+ },
+
_disabledChanged: function(disabled) {
if (disabled) {
this._enabledTabindex = this.getAttribute('tabindex');
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
new file mode 100644
index 0000000..70cf636
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-button</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-button.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-button></gr-button>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-select tests', function() {
+ var element;
+ var sandbox;
+
+ var addSpyOn = function(eventName) {
+ var spy = sandbox.spy();
+ element.addEventListener(eventName, spy);
+ return spy;
+ };
+
+ setup(function() {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(function() {
+ sandbox.restore();
+ });
+
+ ['tap', 'click'].forEach(function(eventName) {
+ test('dispatches ' + eventName + ' event', function() {
+ var spy = addSpyOn(eventName);
+ MockInteractions.tap(element);
+ assert.isTrue(spy.calledOnce);
+ });
+ });
+
+ // Keycodes: 32 for Space, 13 for Enter.
+ [32, 13].forEach(function(key) {
+ test('dispatches tap event on keycode ' + key, function() {
+ var tapSpy = sandbox.spy();
+ element.addEventListener('tap', tapSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, key);
+ assert.isTrue(tapSpy.calledOnce);
+ })});
+
+ suite('disabled', function() {
+ setup(function() {
+ element.disabled = true;
+ });
+
+ ['tap', 'click'].forEach(function(eventName) {
+ test('stops ' + eventName + ' event', function() {
+ var spy = addSpyOn(eventName);
+ MockInteractions.tap(element);
+ assert.isFalse(spy.called);
+ });
+ });
+
+ // Keycodes: 32 for Space, 13 for Enter.
+ [32, 13].forEach(function(key) {
+ test('stops tap event on keycode ' + key, function() {
+ var tapSpy = sandbox.spy();
+ element.addEventListener('tap', tapSpy);
+ MockInteractions.pressAndReleaseKeyOn(element, key);
+ assert.isFalse(tapSpy.called);
+ })});
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 733bf9d..7574d18 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -27,6 +27,9 @@
// Elements tests.
[
+ 'change-list/gr-change-list-item/gr-change-list-item_test.html',
+ 'change-list/gr-change-list-view/gr-change-list-view_test.html',
+ 'change-list/gr-change-list/gr-change-list_test.html',
'change/gr-account-entry/gr-account-entry_test.html',
'change/gr-account-list/gr-account-list_test.html',
'change/gr-change-actions/gr-change-actions_test.html',
@@ -43,19 +46,14 @@
'change/gr-related-changes-list/gr-related-changes-list_test.html',
'change/gr-reply-dialog/gr-reply-dialog_test.html',
'change/gr-reviewer-list/gr-reviewer-list_test.html',
- 'change-list/gr-change-list/gr-change-list_test.html',
- 'change-list/gr-change-list-item/gr-change-list-item_test.html',
- 'change-list/gr-change-list-view/gr-change-list-view_test.html',
'core/gr-account-dropdown/gr-account-dropdown_test.html',
'core/gr-error-manager/gr-error-manager_test.html',
'core/gr-main-header/gr-main-header_test.html',
'core/gr-reporting/gr-reporting_test.html',
'core/gr-search-bar/gr-search-bar_test.html',
- 'diff/gr-diff/gr-diff-group_test.html',
- 'diff/gr-diff/gr-diff_test.html',
'diff/gr-diff-builder/gr-diff-builder_test.html',
- 'diff/gr-diff-comment/gr-diff-comment_test.html',
'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
+ 'diff/gr-diff-comment/gr-diff-comment_test.html',
'diff/gr-diff-cursor/gr-diff-cursor_test.html',
'diff/gr-diff-highlight/gr-annotation_test.html',
'diff/gr-diff-highlight/gr-diff-highlight_test.html',
@@ -63,6 +61,8 @@
'diff/gr-diff-processor/gr-diff-processor_test.html',
'diff/gr-diff-selection/gr-diff-selection_test.html',
'diff/gr-diff-view/gr-diff-view_test.html',
+ 'diff/gr-diff/gr-diff-group_test.html',
+ 'diff/gr-diff/gr-diff_test.html',
'diff/gr-patch-range-select/gr-patch-range-select_test.html',
'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
'diff/gr-selection-action-box/gr-selection-action-box_test.html',
@@ -83,6 +83,7 @@
'shared/gr-alert/gr-alert_test.html',
'shared/gr-autocomplete/gr-autocomplete_test.html',
'shared/gr-avatar/gr-avatar_test.html',
+ 'shared/gr-button/gr-button_test.html',
'shared/gr-change-star/gr-change-star_test.html',
'shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
'shared/gr-cursor-manager/gr-cursor-manager_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index cb6d236..33f33b7 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -32,6 +32,7 @@
restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
port = flag.String("port", ":8081", "Port to serve HTTP requests on")
prod = flag.Bool("prod", false, "Serve production assets")
+ scheme = flag.String("scheme", "https", "URL scheme")
)
func main() {
@@ -57,7 +58,7 @@
req := &http.Request{
Method: "GET",
URL: &url.URL{
- Scheme: "https",
+ Scheme: *scheme,
Host: *restHost,
Opaque: r.URL.EscapedPath(),
RawQuery: r.URL.RawQuery,
diff --git a/tools/intellij/Gerrit_Code_Style.xml b/tools/intellij/Gerrit_Code_Style.xml
new file mode 100644
index 0000000..b913e09
--- /dev/null
+++ b/tools/intellij/Gerrit_Code_Style.xml
@@ -0,0 +1,531 @@
+<code_scheme name="Google Format (Gerrit)">
+ <option name="OTHER_INDENT_OPTIONS">
+ <value>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ <option name="TAB_SIZE" value="2" />
+ <option name="USE_TAB_CHARACTER" value="false" />
+ <option name="SMART_TABS" value="false" />
+ <option name="LABEL_INDENT_SIZE" value="0" />
+ <option name="LABEL_INDENT_ABSOLUTE" value="false" />
+ <option name="USE_RELATIVE_INDENTS" value="false" />
+ </value>
+ </option>
+ <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
+ <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
+ <option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
+ <value />
+ </option>
+ <option name="IMPORT_LAYOUT_TABLE">
+ <value>
+ <package name="" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="com.google" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="org" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="java" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="" withSubpackages="true" static="false" />
+ </value>
+ </option>
+ <option name="RIGHT_MARGIN" value="80" />
+ <option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
+ <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
+ <option name="JD_P_AT_EMPTY_LINES" value="false" />
+ <option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
+ <option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
+ <option name="JD_KEEP_EMPTY_RETURN" value="false" />
+ <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
+ <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
+ <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
+ <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+ <option name="ALIGN_MULTILINE_FOR" value="false" />
+ <option name="CALL_PARAMETERS_WRAP" value="1" />
+ <option name="METHOD_PARAMETERS_WRAP" value="1" />
+ <option name="EXTENDS_LIST_WRAP" value="1" />
+ <option name="THROWS_KEYWORD_WRAP" value="1" />
+ <option name="METHOD_CALL_CHAIN_WRAP" value="1" />
+ <option name="BINARY_OPERATION_WRAP" value="1" />
+ <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+ <option name="TERNARY_OPERATION_WRAP" value="1" />
+ <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+ <option name="FOR_STATEMENT_WRAP" value="1" />
+ <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+ <option name="WRAP_COMMENTS" value="true" />
+ <option name="IF_BRACE_FORCE" value="3" />
+ <option name="DOWHILE_BRACE_FORCE" value="3" />
+ <option name="WHILE_BRACE_FORCE" value="3" />
+ <option name="FOR_BRACE_FORCE" value="3" />
+ <AndroidXmlCodeStyleSettings>
+ <option name="USE_CUSTOM_SETTINGS" value="true" />
+ <option name="LAYOUT_SETTINGS">
+ <value>
+ <option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
+ </value>
+ </option>
+ </AndroidXmlCodeStyleSettings>
+ <JSCodeStyleSettings>
+ <option name="INDENT_CHAINED_CALLS" value="false" />
+ </JSCodeStyleSettings>
+ <Python>
+ <option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" />
+ </Python>
+ <TypeScriptCodeStyleSettings>
+ <option name="INDENT_CHAINED_CALLS" value="false" />
+ </TypeScriptCodeStyleSettings>
+ <XML>
+ <option name="XML_ALIGN_ATTRIBUTES" value="false" />
+ <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
+ </XML>
+ <codeStyleSettings language="CSS">
+ <indentOptions>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="ECMA Script Level 4">
+ <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
+ <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+ <option name="ALIGN_MULTILINE_FOR" value="false" />
+ <option name="CALL_PARAMETERS_WRAP" value="1" />
+ <option name="METHOD_PARAMETERS_WRAP" value="1" />
+ <option name="EXTENDS_LIST_WRAP" value="1" />
+ <option name="BINARY_OPERATION_WRAP" value="1" />
+ <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+ <option name="TERNARY_OPERATION_WRAP" value="1" />
+ <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+ <option name="FOR_STATEMENT_WRAP" value="1" />
+ <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+ <option name="IF_BRACE_FORCE" value="3" />
+ <option name="DOWHILE_BRACE_FORCE" value="3" />
+ <option name="WHILE_BRACE_FORCE" value="3" />
+ <option name="FOR_BRACE_FORCE" value="3" />
+ <option name="PARENT_SETTINGS_INSTALLED" value="true" />
+ </codeStyleSettings>
+ <codeStyleSettings language="HTML">
+ <indentOptions>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="JAVA">
+ <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
+ <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
+ <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="3" />
+ <option name="KEEP_BLANK_LINES_IN_CODE" value="3" />
+ <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="3" />
+ <option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
+ <option name="BLANK_LINES_AROUND_CLASS" value="2" />
+ <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+ <option name="ALIGN_MULTILINE_RESOURCES" value="false" />
+ <option name="ALIGN_MULTILINE_FOR" value="false" />
+ <option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
+ <option name="CALL_PARAMETERS_WRAP" value="1" />
+ <option name="METHOD_PARAMETERS_WRAP" value="1" />
+ <option name="EXTENDS_LIST_WRAP" value="1" />
+ <option name="THROWS_LIST_WRAP" value="1" />
+ <option name="EXTENDS_KEYWORD_WRAP" value="1" />
+ <option name="THROWS_KEYWORD_WRAP" value="1" />
+ <option name="METHOD_CALL_CHAIN_WRAP" value="1" />
+ <option name="BINARY_OPERATION_WRAP" value="1" />
+ <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+ <option name="TERNARY_OPERATION_WRAP" value="1" />
+ <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+ <option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
+ <option name="FOR_STATEMENT_WRAP" value="1" />
+ <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+ <option name="ASSIGNMENT_WRAP" value="1" />
+ <option name="IF_BRACE_FORCE" value="3" />
+ <option name="DOWHILE_BRACE_FORCE" value="3" />
+ <option name="WHILE_BRACE_FORCE" value="3" />
+ <option name="FOR_BRACE_FORCE" value="3" />
+ <option name="PARENT_SETTINGS_INSTALLED" value="true" />
+ <indentOptions>
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="JSON">
+ <indentOptions>
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="JavaScript">
+ <option name="RIGHT_MARGIN" value="80" />
+ <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
+ <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+ <option name="ALIGN_MULTILINE_FOR" value="false" />
+ <option name="CALL_PARAMETERS_WRAP" value="1" />
+ <option name="METHOD_PARAMETERS_WRAP" value="1" />
+ <option name="BINARY_OPERATION_WRAP" value="1" />
+ <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+ <option name="TERNARY_OPERATION_WRAP" value="1" />
+ <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
+ <option name="FOR_STATEMENT_WRAP" value="1" />
+ <option name="ARRAY_INITIALIZER_WRAP" value="1" />
+ <option name="IF_BRACE_FORCE" value="3" />
+ <option name="DOWHILE_BRACE_FORCE" value="3" />
+ <option name="WHILE_BRACE_FORCE" value="3" />
+ <option name="FOR_BRACE_FORCE" value="3" />
+ <option name="PARENT_SETTINGS_INSTALLED" value="true" />
+ <indentOptions>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="Python">
+ <option name="RIGHT_MARGIN" value="80" />
+ <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+ <option name="PARENT_SETTINGS_INSTALLED" value="true" />
+ <indentOptions>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="SASS">
+ <indentOptions>
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="SCSS">
+ <indentOptions>
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="TypeScript">
+ <indentOptions>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ </codeStyleSettings>
+ <codeStyleSettings language="XML">
+ <indentOptions>
+ <option name="INDENT_SIZE" value="2" />
+ <option name="CONTINUATION_INDENT_SIZE" value="2" />
+ <option name="TAB_SIZE" value="2" />
+ </indentOptions>
+ <arrangement>
+ <rules>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:android</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:id</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>style</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:.*Style</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_width</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_height</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_weight</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_margin</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_marginTop</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_marginBottom</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_marginStart</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_marginEnd</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_marginLeft</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_marginRight</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:layout_.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:padding</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:paddingTop</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:paddingBottom</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:paddingStart</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:paddingEnd</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:paddingLeft</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:paddingRight</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_NAMESPACE>.*</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ </rules>
+ </arrangement>
+ </codeStyleSettings>
+</code_scheme>
diff --git a/tools/intellij/copyright/Gerrit_Copyright.xml b/tools/intellij/copyright/Gerrit_Copyright.xml
new file mode 100644
index 0000000..5609cdc
--- /dev/null
+++ b/tools/intellij/copyright/Gerrit_Copyright.xml
@@ -0,0 +1,6 @@
+<component name="CopyrightManager">
+ <copyright>
+ <option name="myName" value="Gerrit Copyright" />
+ <option name="notice" value="Copyright (C) &#36;today.year 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." />
+ </copyright>
+</component>
\ No newline at end of file
diff --git a/tools/intellij/copyright/profiles_settings.xml b/tools/intellij/copyright/profiles_settings.xml
new file mode 100644
index 0000000..dfb94d5
--- /dev/null
+++ b/tools/intellij/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+<component name="CopyrightManager">
+ <settings default="Gerrit Copyright">
+ <LanguageOptions name="__TEMPLATE__">
+ <option name="block" value="false" />
+ </LanguageOptions>
+ </settings>
+</component>
\ No newline at end of file