Merge "Set maxObjectSizeLimit when project is created via REST"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 5102033..9ff0156 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -419,8 +419,9 @@
to projects in Gerrit. It can give permission to abandon a specific
change to a given ref.
-This also grants the permission to restore a change if the change
-can be uploaded.
+This also grants the permission to restore a change if the user also
+has link:#category_push[push permission] on the change's destination
+ref.
[[category_create]]
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 8de8e59..53c9750 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -65,6 +65,13 @@
related to a user editing the commit message through the Gerrit UI. It is a
`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+Footer.vm
+~~~~~~~~~
+
+The `Footer.vm` template will determine the contents of the footer text
+appended to the end of all outgoing emails after the ChangeFooter and
+CommentFooter.
+
Merged.vm
~~~~~~~~~
diff --git a/Documentation/error-missing-changeid.txt b/Documentation/error-missing-changeid.txt
index b13f3b4..edbc63b 100644
--- a/Documentation/error-missing-changeid.txt
+++ b/Documentation/error-missing-changeid.txt
@@ -6,11 +6,12 @@
message if the commit message of the pushed commit does not contain
a Change-Id in the footer (the last paragraph).
-This error may happen for two reasons:
+This error may happen for different reasons:
. missing Change-Id in the commit message
. Change-Id is contained in the commit message but not in the last
paragraph
+. Change-Id is the only line in the commit message
You can see the commit messages for existing commits in the history
by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log].
@@ -51,6 +52,20 @@
Change-ID into the last paragraph. How to update the commit message
is explained link:error-push-fails-due-to-commit-message.html[here].
+Change-Id is the only line in the commit message
+------------------------------------------------
+
+Gerrit does not parse the subject of a commit message for the
+Change-Id even if this is the only and last paragraph of the commit
+message.
+
+If the Change-Id is the only line in the commit message you must update
+the commit message and insert a subject as the first line in the commit
+message. The Change-Id must be in the last paragraph of the commit
+message, i.e. separated from the subject by a blank line. How to update
+the commit message is explained
+link:error-push-fails-due-to-commit-message.html[here].
+
GERRIT
------
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 700b145..2836e5b 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -115,6 +115,8 @@
createdOn:: Time in seconds since the UNIX epoch when this patchset
was created.
+isDraft:: Whether or not the patch set is a draft patch set.
+
approvals:: The <<approval,approval attribute>> granted.
comments:: All comments for this patchset in <<patchsetcomment,patchsetComment attributes>>.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 1d8b934..c0cf578 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -364,6 +364,11 @@
detailed labels], link:#detailed-accounts[detailed accounts], and
link:#messages[messages].
+Additional fields can be obtained by adding `o` parameters, each
+option requires more database lookups and slows down the query
+response time to the client so they are generally disabled by
+default. Fields are described in link:#list-changes[Query Changes].
+
.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/detail HTTP/1.0
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 70f564c..6af6897 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -9,6 +9,28 @@
Config Endpoints
---------------
+[[get-version]]
+Get Version
+~~~~~~~~~~~
+[verse]
+'GET /config/server/version'
+
+Returns the version of the Gerrit server.
+
+.Request
+----
+ GET /config/server/version HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ "2.7"
+----
+
[[list-capabilities]]
List Capabilities
~~~~~~~~~~~~~~~~~
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 711d1ac..558dcb5 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -22,6 +22,14 @@
change notifications to specific subsets, for example `branch:master`
to only see changes proposed for the master branch.
+Notification mails for new changes and new patch sets are not sent to
+the change owner.
+
+Notification mails for comments added on changes are not sent to the user
+who added the comment unless the user has enabled the 'CC Me On Comments I
+Write' option in the user preferences.
+
+
Project Level Settings
----------------------
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
index c321814..b7ebc0b 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -29,6 +29,18 @@
------------
+Configuration
+~~~~~~~~~~~~~
+
+* Project owners can define `receive.maxObjectSizeLimit` in the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config-gerrit.html#receive.maxObjectSizeLimit[
+project configuration] to further reduce the global setting.
+
+* Site administrators can define a
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config-mail.html#_footer_vm[
+footer template] that will be appended to the end of all outgoing emails after
+the 'ChangeFooter' and 'CommentFooter'.
+
Web UI
~~~~~~
@@ -154,6 +166,9 @@
* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-config.html#get-capabilities[
Get capabilities]
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-config.html#get-version[
+Get version] (of the Gerrit server)
+
Projects
^^^^^^^^
@@ -185,7 +200,7 @@
New global capabilities are added.
* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/access_control.html#capability_generateHttpPassword[
-Generate Http Password] Allows non-administrator users to generate HTTP
+Generate Http Password] allows non-administrator users to generate HTTP
passwords for users other than themselves.
+
This capability would typically be assigned to a non-interactive group
@@ -193,7 +208,7 @@
that uses the Gerrit REST API.
* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/access_control.html#capability_runAs[
-Run As] Allows users to impersonate other users by setting the `X-Gerrit-RunAs`
+Run As] allows users to impersonate other users by setting the `X-Gerrit-RunAs`
HTTP header on REST API calls.
+
Site administrators do not inherit this capability; it must be granted
@@ -204,11 +219,25 @@
~~~~~~~
-* The commit message length checker plugin can be configured to reject
-commits whose subject or body length exceeds the limit.
+Global
+^^^^^^
+
* Plugins may now contribute buttons to various parts of the UI.
+Commit Message Length Checker
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+
+* Commits whose subject or body length exceeds the limit can be rejected.
+
+Replication
+^^^^^^^^^^^
+
+* The `{$name}` placeholder is optional when replicating a single project,
+allowing a single project to be replicated under a different name.
+
+* Projects can be matched with wildcard or regex patterns in replication.config.
ssh
~~~
@@ -222,7 +251,7 @@
New `ls-members` command].
* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/cmd-set-members.html[
-New `set-members` command]
+New `set-members` command].
+
New command to manipulate group membership. Members can be added or removed
and groups can be included or excluded in one specific group or number of groups.
@@ -253,6 +282,9 @@
* link:https://code.google.com/p/gerrit/issues/detail?id=1574[Issue 1574]:
Correctly highlight matches of text in escaped HTML entities in suggestion results.
+* link:https://code.google.com/p/gerrit/issues/detail?id=1996[Issue 1996]:
+The "Keyboard Shortcuts" help popup can be closed by pressing the Escape key.
+
Change Screens
^^^^^^^^^^^^^^
@@ -302,6 +334,14 @@
* link:https://code.google.com/p/gerrit/issues/detail?id=1908[Issue 1908]:
Provide more informative error messages when rejecting updates.
+* Remove the limit in the query of patch sets by revision.
+
+* Add `isDraft` in the `patchSet` attribute of stream-events data.
++
+This allows consumers of the event stream to determine whether or not
+the event is related to a draft patch set.
+
+
Tools
~~~~~
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
index 07bbcae..9a8c1d8 100644
--- a/gerrit-common/BUCK
+++ b/gerrit-common/BUCK
@@ -42,7 +42,7 @@
# TODO(sop): Move git describe into an uncacheable genrule()
def git_describe():
import subprocess
- cmd = ['git', 'describe', 'HEAD']
+ cmd = ['git', 'describe', '--match', 'v[0-9].*', '--dirty']
p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
v = p.communicate()[0].strip()
r = p.returncode
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java
new file mode 100644
index 0000000..a8ffe94
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2013 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.extensions.restapi;
+
+import java.util.concurrent.TimeUnit;
+
+public class CacheControl {
+
+ public enum Type {
+ NONE, PUBLIC, PRIVATE;
+ }
+
+ public final static CacheControl NONE = new CacheControl(Type.NONE, 0, null);
+
+ public static CacheControl PUBLIC(long age, TimeUnit unit) {
+ return new CacheControl(Type.PUBLIC, age, unit);
+ }
+
+ public static CacheControl PRIVATE(long age, TimeUnit unit) {
+ return new CacheControl(Type.PRIVATE, age, unit);
+ }
+
+ private final Type type;
+ private final long age;
+ private final TimeUnit unit;
+
+ private CacheControl(Type type, long age, TimeUnit unit) {
+ this.type = type;
+ this.age = age;
+ this.unit = unit;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public long getAge() {
+ return age;
+ }
+
+ public TimeUnit getUnit() {
+ return unit;
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
index aa891c9..0e358ec 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
@@ -31,4 +31,10 @@
public ResourceNotFoundException(IdString id) {
super(id.get());
}
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ResourceNotFoundException caching(CacheControl c) {
+ return super.caching(c);
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
index 80ca08a..848004d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
@@ -19,10 +19,6 @@
@SuppressWarnings({"rawtypes"})
private static final Response NONE = new None();
- public enum CacheControl {
- NONE, PUBLIC, PRIVATE;
- }
-
/** HTTP 200 OK: pointless wrapper for type safety. */
public static <T> Response<T> ok(T value) {
return new Impl<T>(200, value);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
index a6d27cd..3fae128 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
@@ -17,6 +17,7 @@
/** Root exception type for JSON API failures. */
public abstract class RestApiException extends Exception {
private static final long serialVersionUID = 1L;
+ private CacheControl caching = CacheControl.NONE;
public RestApiException() {
}
@@ -28,4 +29,14 @@
public RestApiException(String msg, Throwable cause) {
super(msg, cause);
}
+
+ public CacheControl caching() {
+ return caching;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T extends RestApiException> T caching(CacheControl c) {
+ caching = c;
+ return (T) this;
+ }
}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
index 7bd0233..6f8bf80 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -16,8 +16,11 @@
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlowPanel;
@@ -38,7 +41,7 @@
public class KeyHelpPopup extends PluginSafePopupPanel implements
- KeyPressHandler {
+ KeyPressHandler, KeyUpHandler {
private final FocusPanel focus;
public KeyHelpPopup() {
@@ -77,6 +80,7 @@
DOM.setStyleAttribute(focus.getElement(), "outline", "0px");
DOM.setElementAttribute(focus.getElement(), "hideFocus", "true");
focus.addKeyPressHandler(this);
+ focus.addKeyUpHandler(this);
add(focus);
}
@@ -100,6 +104,13 @@
hide();
}
+ @Override
+ public void onKeyUp(final KeyUpEvent event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+ hide();
+ }
+ }
+
private void populate(final Grid lists) {
int end[] = new int[5];
int column = 0;
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
index 0a9f7a2..41e4abc 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -29,7 +29,9 @@
import java.util.List;
/** Immutable string safely placed as HTML without further escaping. */
-public abstract class SafeHtml {
+@SuppressWarnings("serial")
+public abstract class SafeHtml
+ implements com.google.gwt.safehtml.shared.SafeHtml {
public static final SafeHtmlResources RESOURCES;
static {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
index 9fe3267..8ff99ee 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -19,6 +19,7 @@
/**
* Safely constructs a {@link SafeHtml}, escaping user provided content.
*/
+@SuppressWarnings("serial")
public class SafeHtmlBuilder extends SafeHtml {
private static final Impl impl;
@@ -317,6 +318,16 @@
return closeElement("td");
}
+ /** Append "<th>"; attributes may be set if needed */
+ public SafeHtmlBuilder openTh() {
+ return openElement("th");
+ }
+
+ /** Append "</th>" */
+ public SafeHtmlBuilder closeTh() {
+ return closeElement("th");
+ }
+
/** Append "<div>"; attributes may be set if needed */
public SafeHtmlBuilder openDiv() {
return openElement("div");
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
index a229421..57392bf 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
@@ -14,6 +14,7 @@
package com.google.gwtexpui.safehtml.client;
+@SuppressWarnings("serial")
class SafeHtmlString extends SafeHtml {
private final String html;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index 9fa89e6..5dcccb0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -81,8 +81,7 @@
});
if (addPopup) {
- UserPopupPanel userPopup = new UserPopupPanel(account, false, false);
- PopupHandler popupHandler = new PopupHandler(userPopup, this);
+ PopupHandler popupHandler = new PopupHandler(account, this);
addMouseOverHandler(popupHandler);
addMouseOutHandler(popupHandler);
}
@@ -115,16 +114,20 @@
}
private class PopupHandler implements MouseOverHandler, MouseOutHandler {
- private final UserPopupPanel popup;
+ private final AccountInfo account;
private final UIObject target;
+ private UserPopupPanel popup;
private Timer showTimer;
private Timer hideTimer;
- public PopupHandler(UserPopupPanel popup, UIObject target) {
- this.popup = popup;
+ public PopupHandler(AccountInfo account, UIObject target) {
+ this.account = account;
this.target = target;
+ }
+ private UserPopupPanel createPopupPanel(AccountInfo account) {
+ UserPopupPanel popup = new UserPopupPanel(account, false, false);
popup.addDomHandler(new MouseOverHandler() {
@Override
public void onMouseOver(MouseOverEvent event) {
@@ -137,6 +140,7 @@
scheduleHide();
}
}, MouseOutEvent.getType());
+ return popup;
}
@Override
@@ -154,12 +158,16 @@
hideTimer.cancel();
hideTimer = null;
}
- if ((popup.isShowing() && popup.isVisible()) || showTimer != null) {
+ if ((popup != null && popup.isShowing() && popup.isVisible())
+ || showTimer != null) {
return;
}
showTimer = new Timer() {
@Override
public void run() {
+ if (popup == null) {
+ popup = createPopupPanel(account);
+ }
if (!popup.isShowing() || !popup.isVisible()) {
popup.showRelativeTo(target);
}
@@ -174,7 +182,8 @@
showTimer.cancel();
showTimer = null;
}
- if (!popup.isShowing() || !popup.isVisible() || hideTimer != null) {
+ if (popup == null || !popup.isShowing() || !popup.isVisible()
+ || hideTimer != null) {
return;
}
hideTimer = new Timer() {
@@ -186,6 +195,4 @@
hideTimer.schedule(50);
}
}
-
-
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index ea836c6..ac455c7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -120,8 +120,9 @@
ownerTxtBox = new NpTextBox();
ownerTxtBox.setVisibleLength(60);
+ final AccountGroupSuggestOracle accountGroupOracle = new AccountGroupSuggestOracle();
ownerTxt = new SuggestBox(new RPCSuggestOracle(
- new AccountGroupSuggestOracle()), ownerTxtBox);
+ accountGroupOracle), ownerTxtBox);
ownerTxt.setStyleName(Gerrit.RESOURCES.css().groupOwnerTextBox());
ownerPanel.add(ownerTxt);
@@ -132,7 +133,9 @@
public void onClick(final ClickEvent event) {
final String newOwner = ownerTxt.getText().trim();
if (newOwner.length() > 0) {
- GroupApi.setGroupOwner(getGroupUUID(), newOwner,
+ AccountGroup.UUID ownerUuid = accountGroupOracle.getUUID(newOwner);
+ String ownerId = ownerUuid != null ? ownerUuid.get() : newOwner;
+ GroupApi.setGroupOwner(getGroupUUID(), ownerId,
new GerritCallback<GroupInfo>() {
public void onSuccess(final GroupInfo result) {
updateOwnerGroup(result);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
index 76a0cfb..de84081 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -29,6 +29,7 @@
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwt.user.client.ui.HTMLTable.Cell;
@@ -77,7 +78,12 @@
@Override
protected void onOpenRow(final int row) {
- History.newItem(Dispatcher.toGroup(getRowItem(row).getGroupId()));
+ GroupInfo groupInfo = getRowItem(row);
+ if (isInteralGroup(groupInfo)) {
+ History.newItem(Dispatcher.toGroup(groupInfo.getGroupId()));
+ } else if (groupInfo.url() != null) {
+ Window.open(groupInfo.url(), "_self", null);
+ }
}
public void display(GroupMap groups, String toHighlight) {
@@ -108,7 +114,7 @@
void populate(final int row, final GroupInfo k, final String toHighlight) {
if (k.url() != null) {
- if (k.url().startsWith("#" + PageLinks.ADMIN_GROUPS)) {
+ if (isInteralGroup(k)) {
table.setWidget(row, 1, new HighlightingInlineHyperlink(k.name(),
Dispatcher.toGroup(k.getGroupId()), toHighlight));
} else {
@@ -133,4 +139,9 @@
setRowItem(row, k);
}
+
+ private boolean isInteralGroup(final GroupInfo groupInfo) {
+ return groupInfo != null
+ && groupInfo.url().startsWith("#" + PageLinks.ADMIN_GROUPS);
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index 9375e44..2ab1a8a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -65,6 +65,10 @@
call(id, "detail").get(cb);
}
+ public static RestApi revision(int id, String revision) {
+ return change(id).view("revisions").id(revision);
+ }
+
public static RestApi revision(PatchSet.Id id) {
return change(id.getParentKey().get()).view("revisions").id(id.get());
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
index 7c41ea1..ab53f5b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -34,7 +34,7 @@
for (String q : queries) {
call.addParameterRaw("q", KeyUtil.encode(q));
}
- addOptions(call, ListChangesOption.LABELS);
+ addOptions(call, EnumSet.of(ListChangesOption.LABELS));
call.get(callback);
}
@@ -45,7 +45,7 @@
if (limit > 0) {
call.addParameter("n", limit);
}
- addOptions(call, ListChangesOption.LABELS);
+ addOptions(call, EnumSet.of(ListChangesOption.LABELS));
if (!PagedSingleListScreen.MIN_SORTKEY.equals(sortkey)) {
call.addParameter("P", sortkey);
}
@@ -59,16 +59,14 @@
if (limit > 0) {
call.addParameter("n", limit);
}
- addOptions(call, ListChangesOption.LABELS);
+ addOptions(call, EnumSet.of(ListChangesOption.LABELS));
if (!PagedSingleListScreen.MAX_SORTKEY.equals(sortkey)) {
call.addParameter("N", sortkey);
}
call.get(callback);
}
- private static void addOptions(
- RestApi call, ListChangesOption option1, ListChangesOption... options) {
- EnumSet<ListChangesOption> s = EnumSet.of(option1, options);
+ static void addOptions(RestApi call, EnumSet<ListChangesOption> s) {
call.addParameterRaw("O", Integer.toHexString(ListChangesOption.toBits(s)));
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
index 1b437ae..d87cb5e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
@@ -17,6 +17,7 @@
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -28,7 +29,7 @@
}
public static void comment(PatchSet.Id id, String commentId,
- AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
+ AsyncCallback<CommentInfo> cb) {
revision(id, "comments").id(commentId).get(cb);
}
@@ -38,22 +39,22 @@
}
public static void draft(PatchSet.Id id, String draftId,
- AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
+ AsyncCallback<CommentInfo> cb) {
revision(id, "drafts").id(draftId).get(cb);
}
- public static void createDraft(PatchSet.Id id, CommentInfo content,
- AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
+ public static void createDraft(PatchSet.Id id, CommentInput content,
+ AsyncCallback<CommentInfo> cb) {
revision(id, "drafts").put(content, cb);
}
public static void updateDraft(PatchSet.Id id, String draftId,
- CommentInfo content, AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
+ CommentInput content, AsyncCallback<CommentInfo> cb) {
revision(id, "drafts").id(draftId).put(content, cb);
}
public static void deleteDraft(PatchSet.Id id, String draftId,
- AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
+ AsyncCallback<JavaScriptObject> cb) {
revision(id, "drafts").id(draftId).delete(cb);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
index 7e229f6..b1f3a2a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -22,6 +22,32 @@
import java.sql.Timestamp;
public class CommentInfo extends JavaScriptObject {
+ public static CommentInfo create(String path, Side side, int line,
+ String in_reply_to, String message) {
+ CommentInfo info = createObject().cast();
+ info.setPath(path);
+ info.setSide(side);
+ info.setLine(line);
+ info.setInReplyTo(in_reply_to);
+ info.setMessage(message);
+ return info;
+ }
+
+ private final native void setId(String id) /*-{ this.id = id; }-*/;
+ private final native void setPath(String path) /*-{ this.path = path; }-*/;
+
+ private final void setSide(Side side) {
+ setSideRaw(side.toString());
+ }
+ private final native void setSideRaw(String side) /*-{ this.side = side; }-*/;
+
+ private final native void setLine(int line) /*-{ this.line = line; }-*/;
+
+ private final native void setInReplyTo(String in_reply_to) /*-{
+ this.in_reply_to = in_reply_to;
+ }-*/;
+
+ private final native void setMessage(String message) /*-{ this.message = message; }-*/;
public final native String id() /*-{ return this.id; }-*/;
public final native String path() /*-{ return this.path; }-*/;
@@ -39,7 +65,10 @@
public final native String message() /*-{ return this.message; }-*/;
public final Timestamp updated() {
- return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
+ String updatedRaw = updatedRaw();
+ return updatedRaw == null
+ ? null
+ : JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
}
private final native String updatedRaw() /*-{ return this.updated; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java
new file mode 100644
index 0000000..592e087
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2013 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.client.changes;
+
+import com.google.gerrit.common.changes.Side;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+
+public class CommentInput extends JavaScriptObject {
+ public static CommentInput create(CommentInfo original) {
+ CommentInput input = createObject().cast();
+ input.setId(original.id());
+ input.setPath(original.path());
+ input.setSide(original.side());
+ if (original.has_line()) {
+ input.setLine(original.line());
+ }
+ input.setInReplyTo(original.in_reply_to());
+ input.setMessage(original.message());
+ return input;
+ }
+
+ public final native void setId(String id) /*-{ this.id = id; }-*/;
+ public final native void setPath(String path) /*-{ this.path = path; }-*/;
+
+ public final void setSide(Side side) {
+ setSideRaw(side.toString());
+ }
+ private final native void setSideRaw(String side) /*-{ this.side = side; }-*/;
+
+ public final native void setLine(int line) /*-{ this.line = line; }-*/;
+
+ public final native void setInReplyTo(String in_reply_to) /*-{
+ this.in_reply_to = in_reply_to;
+ }-*/;
+
+ public final native void setMessage(String message) /*-{ this.message = message; }-*/;
+ public final native String id() /*-{ return this.id; }-*/;
+ public final native String path() /*-{ return this.path; }-*/;
+
+ public final Side side() {
+ String s = sideRaw();
+ return s != null
+ ? Side.valueOf(s)
+ : Side.REVISION;
+ }
+ private final native String sideRaw() /*-{ return this.side }-*/;
+
+ public final native int line() /*-{ return this.line; }-*/;
+ public final native String in_reply_to() /*-{ return this.in_reply_to; }-*/;
+ public final native String message() /*-{ return this.message; }-*/;
+
+ public final Timestamp updated() {
+ return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
+ }
+ private final native String updatedRaw() /*-{ return this.updated; }-*/;
+
+ public final native boolean has_line() /*-{ return this.hasOwnProperty('line'); }-*/;
+
+ protected CommentInput() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java
index ea26f28..90973ae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CodeMirrorDemo.java
@@ -14,22 +14,27 @@
package com.google.gerrit.client.diff;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.CommentApi;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.diff.DiffInfo.Region;
import com.google.gerrit.client.diff.DiffInfo.Span;
+import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
+import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.changes.Side;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.logical.shared.ResizeEvent;
@@ -37,20 +42,33 @@
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.LineClassWhere;
import net.codemirror.lib.Configuration;
+import net.codemirror.lib.KeyMap;
import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.LineWidget;
import net.codemirror.lib.ModeInjector;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class CodeMirrorDemo extends Screen {
private static final int HEADER_FOOTER = 60 + 15 * 2 + 38;
private static final JsArrayString EMPTY =
JavaScriptObject.createArray().cast();
+ /**
+ * TODO: Handle ordering of line widgets. CodeMirror seems to have a bug when
+ * multiple line widgets are added to a line: it puts the first widget above
+ * the line when the line is clicked upon.
+ */
+ private static final Configuration COMMENT_BOX_CONFIG =
+ Configuration.create().set("coverGutter", true);
+
private final PatchSet.Id base;
private final PatchSet.Id revision;
private final String path;
@@ -61,8 +79,13 @@
private HandlerRegistration resizeHandler;
private JsArray<CommentInfo> published;
private JsArray<CommentInfo> drafts;
- private List<Runnable> resizeCallbacks;
+ private List<CommentBox> initialBoxes;
+ private DiffInfo diff;
private LineMapper mapper;
+ private CommentLinkProcessor commentLinkProcessor;
+ private Map<String, PublishedBox> publishedMap;
+ private Map<Integer, CommentBox> lineBoxMapA;
+ private Map<Integer, CommentBox> lineBoxMapB;
public CodeMirrorDemo(
PatchSet.Id base,
@@ -84,29 +107,25 @@
protected void onLoad() {
super.onLoad();
- CallbackGroup group = new CallbackGroup();
- CodeMirror.initLibrary(group.add(new GerritCallback<Void>() {
- @Override
- public void onSuccess(Void result) {
- }
- }));
+ CallbackGroup cmGroup = new CallbackGroup();
+ CodeMirror.initLibrary(cmGroup.add(CallbackGroup.<Void>emptyCallback()));
+ final CallbackGroup group = new CallbackGroup();
+ final AsyncCallback<Void> modeInjectorCb =
+ group.add(CallbackGroup.<Void>emptyCallback());
+
DiffApi.diff(revision, path)
.base(base)
.wholeFile()
.intraline()
.ignoreWhitespace(DiffApi.IgnoreWhitespace.NONE)
- .get(group.addFinal(new GerritCallback<DiffInfo>() {
+ .get(cmGroup.addFinal(new GerritCallback<DiffInfo>() {
@Override
- public void onSuccess(final DiffInfo diff) {
+ public void onSuccess(DiffInfo diffInfo) {
+ diff = diffInfo;
new ModeInjector()
.add(getContentType(diff.meta_a()))
.add(getContentType(diff.meta_b()))
- .inject(new ScreenLoadCallback<Void>(CodeMirrorDemo.this) {
- @Override
- protected void preDisplay(Void result) {
- display(diff);
- }
- });
+ .inject(modeInjectorCb);
}
}));
CommentApi.comments(revision,
@@ -114,11 +133,33 @@
@Override
public void onSuccess(NativeMap<JsArray<CommentInfo>> m) { published = m.get(path); }
}));
- CommentApi.drafts(revision,
- group.add(new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
+ if (Gerrit.isSignedIn()) {
+ CommentApi.drafts(revision,
+ group.add(new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
+ @Override
+ public void onSuccess(NativeMap<JsArray<CommentInfo>> m) { drafts = m.get(path); }
+ }));
+ } else {
+ drafts = JsArray.createArray().cast();
+ }
+ ChangeApi.detail(revision.getParentKey().get(), new GerritCallback<ChangeInfo>() {
@Override
- public void onSuccess(NativeMap<JsArray<CommentInfo>> m) { drafts = m.get(path); }
- }));
+ public void onSuccess(ChangeInfo result) {
+ Project.NameKey project = result.project_name_key();
+ ConfigInfoCache.get(project, group.addFinal(
+ new ScreenLoadCallback<ConfigInfoCache.Entry>(CodeMirrorDemo.this) {
+ @Override
+ protected void preDisplay(ConfigInfoCache.Entry result) {
+ commentLinkProcessor = result.getCommentLinkProcessor();
+ setTheme(result.getTheme());
+
+ DiffInfo diffInfo = diff;
+ diff = null;
+ display(diffInfo);
+ }
+ }));
+ }
+ });
}
@Override
@@ -131,20 +172,15 @@
cmB.refresh();
}
Window.enableScrolling(false);
- Scheduler.get().scheduleDeferred(new ScheduledCommand() {
- @Override
- public void execute() {
- for (Runnable r : resizeCallbacks) {
- r.run();
- }
- resizeCallbacks = null;
- }
- });
+ for (CommentBox box : initialBoxes) {
+ box.resizePaddingWidget();
+ }
}
@Override
protected void onUnload() {
super.onUnload();
+
if (resizeHandler != null) {
resizeHandler.removeHandler();
resizeHandler = null;
@@ -160,17 +196,24 @@
Window.enableScrolling(true);
}
- private void display(DiffInfo diff) {
- cmA = displaySide(diff.meta_a(), diff.text_a(), diffTable.getCmA());
- cmB = displaySide(diff.meta_b(), diff.text_b(), diffTable.getCmB());
- render(diff);
- resizeCallbacks = new ArrayList<Runnable>();
- renderComments(published, false);
- renderComments(drafts, true);
+ private void display(DiffInfo diffInfo) {
+ cmA = displaySide(diffInfo.meta_a(), diffInfo.text_a(), diffTable.getCmA());
+ cmB = displaySide(diffInfo.meta_b(), diffInfo.text_b(), diffTable.getCmB());
+ render(diffInfo);
+ initialBoxes = new ArrayList<CommentBox>();
+ publishedMap = new HashMap<String, PublishedBox>(published.length());
+ lineBoxMapA = new HashMap<Integer, CommentBox>();
+ lineBoxMapB = new HashMap<Integer, CommentBox>();
+ renderPublished();
+ renderDrafts();
published = null;
drafts = null;
- mapper = null;
-
+ cmA.on("cursorActivity", updateActiveLine(cmA));
+ cmB.on("cursorActivity", updateActiveLine(cmB));
+ if (Gerrit.isSignedIn()) {
+ cmA.addKeyMap(KeyMap.create().on("'c'", insertNewDraft(cmA)));
+ cmB.addKeyMap(KeyMap.create().on("'c'", insertNewDraft(cmB)));
+ }
// TODO: Probably need horizontal resize
resizeHandler = Window.addResizeHandler(new ResizeHandler() {
@Override
@@ -239,32 +282,88 @@
}
}
- private void renderComments(JsArray<CommentInfo> comments, boolean isDraft) {
- Configuration config = Configuration.create().set("coverGutter", true);
- for (int i = 0; comments != null && i < comments.length(); i++) {
- CommentInfo info = comments.get(i);
- Side mySide = info.side();
- CodeMirror cm = mySide == Side.PARENT ? cmA : cmB;
- CodeMirror other = otherCM(cm);
- final CommentBox box = new CommentBox(info.author(), info.updated(),
- info.message(), isDraft);
- int line = info.line() - 1; // CommentInfo is 1-based, but CM is 0-based
- diffTable.add(box);
- cm.addLineWidget(line, box.getElement(), config);
- int lineToPad = mapper.lineOnOther(mySide, line);
- // Estimated height at 21px, fixed by deferring after display
- final Element paddingOtherside = addPaddingWidget(other,
- diffTable.style.padding(), lineToPad,
- 21, Unit.PX);
- Runnable callback = new Runnable() {
- @Override
- public void run() {
- paddingOtherside.getStyle().setHeight(
- box.getOffsetHeight(), Unit.PX);
- }
- };
- resizeCallbacks.add(callback);
- box.setOpenCloseHandler(callback);
+ private DraftBox addNewDraft(CodeMirror cm, int line) {
+ Side side = cm == cmA ? Side.PARENT : Side.REVISION;
+ CommentInfo info = CommentInfo.create(
+ path,
+ side,
+ line + 1,
+ null,
+ null);
+ return addDraftBox(info, false);
+ }
+
+ DraftBox addReply(CommentInfo replyTo, String initMessage, boolean doSave) {
+ Side side = replyTo.side();
+ int line = replyTo.line();
+ CommentInfo info = CommentInfo.create(
+ path,
+ side,
+ line,
+ replyTo.id(),
+ initMessage);
+ return addDraftBox(info, doSave);
+ }
+
+ private DraftBox addDraftBox(CommentInfo info, boolean doSave) {
+ DraftBox box = new DraftBox(this, revision, info, commentLinkProcessor,
+ true, doSave);
+ addCommentBox(info, box);
+ if (!doSave) {
+ box.setEdit(true);
+ }
+ getLineBoxMapFromSide(info.side()).put(info.line() - 1, box);
+ return box;
+ }
+
+ CommentBox addCommentBox(CommentInfo info, final CommentBox box) {
+ diffTable.add(box);
+ Side mySide = info.side();
+ CodeMirror cm = mySide == Side.PARENT ? cmA : cmB;
+ CodeMirror other = otherCM(cm);
+ int line = info.line() - 1; // CommentInfo is 1-based, but CM is 0-based
+ LineWidget boxWidget =
+ cm.addLineWidget(line, box.getElement(), COMMENT_BOX_CONFIG);
+ int lineToPad = mapper.lineOnOther(mySide, line).getLine();
+ // Estimated height at 21px, fixed by deferring after display
+ LineWidgetElementPair padding = addPaddingWidget(
+ other, diffTable.style.padding(), lineToPad, 21, Unit.PX);
+ box.setSelfWidget(boxWidget);
+ box.setPadding(padding.widget, padding.element);
+ return box;
+ }
+
+ void removeCommentBox(Side side, int line) {
+ getLineBoxMapFromSide(side).remove(line);
+ }
+
+ private void renderPublished() {
+ for (int i = 0; published != null && i < published.length(); i++) {
+ CommentInfo info = published.get(i);
+ final PublishedBox box =
+ new PublishedBox(this, revision, info, commentLinkProcessor);
+ box.setOpen(false);
+ addCommentBox(info, box);
+ initialBoxes.add(box);
+ publishedMap.put(info.id(), box);
+ getLineBoxMapFromSide(info.side()).put(info.line() - 1, box);
+ }
+ }
+
+ private void renderDrafts() {
+ for (int i = 0; drafts != null && i < drafts.length(); i++) {
+ CommentInfo info = drafts.get(i);
+ final DraftBox box =
+ new DraftBox(this, revision, info, commentLinkProcessor, false, false);
+ box.setOpen(false);
+ box.setEdit(false);
+ addCommentBox(info, box);
+ initialBoxes.add(box);
+ PublishedBox replyToBox = publishedMap.get(info.in_reply_to());
+ if (replyToBox != null) {
+ replyToBox.registerReplyBox(box);
+ }
+ getLineBoxMapFromSide(info.side()).put(info.line() - 1, box);
}
}
@@ -272,6 +371,10 @@
return me == cmA ? cmB : cmA;
}
+ private Map<Integer, CommentBox> getLineBoxMapFromSide(Side side) {
+ return side == Side.PARENT ? lineBoxMapA : lineBoxMapB;
+ }
+
private void markEdit(CodeMirror cm, JsArrayString lines,
JsArray<Span> edits, int startLine) {
if (edits == null) {
@@ -316,16 +419,16 @@
cnt, Unit.EM);
}
- private Element addPaddingWidget(CodeMirror cm, String style, int line,
- int height, Unit unit) {
+ private LineWidgetElementPair addPaddingWidget(CodeMirror cm, String style,
+ int line, int height, Unit unit) {
Element div = DOM.createDiv();
div.setClassName(style);
div.getStyle().setHeight(height, unit);
Configuration config = Configuration.create()
.set("coverGutter", true)
.set("above", line == -1);
- cm.addLineWidget(line == -1 ? 0 : line, div, config);
- return div;
+ LineWidget widget = cm.addLineWidget(line == -1 ? 0 : line, div, config);
+ return new LineWidgetElementPair(widget, div);
}
private Runnable doScroll(final CodeMirror cm) {
@@ -337,12 +440,74 @@
};
}
+ private Runnable updateActiveLine(final CodeMirror cm) {
+ final CodeMirror other = otherCM(cm);
+ return new Runnable() {
+ public void run() {
+ if (cm.hasActiveLine()) {
+ cm.removeLineClass(cm.getActiveLine(),
+ LineClassWhere.WRAP, diffTable.style.activeLine());
+ cm.removeLineClass(cm.getActiveLine(),
+ LineClassWhere.BACKGROUND, diffTable.style.activeLineBg());
+ }
+ if (other.hasActiveLine()) {
+ other.removeLineClass(other.getActiveLine(),
+ LineClassWhere.WRAP, diffTable.style.activeLine());
+ other.removeLineClass(other.getActiveLine(),
+ LineClassWhere.BACKGROUND, diffTable.style.activeLineBg());
+ }
+ int line = cm.getCursor("head").getLine();
+ LineOnOtherInfo info =
+ mapper.lineOnOther(cm == cmA ? Side.PARENT : Side.REVISION, line);
+ int oLine = info.getLine();
+ cm.setActiveLine(line);
+ cm.addLineClass(line, LineClassWhere.WRAP, diffTable.style.activeLine());
+ cm.addLineClass(line, LineClassWhere.BACKGROUND, diffTable.style.activeLineBg());
+ if (info.isAligned()) {
+ other.setActiveLine(oLine);
+ other.addLineClass(oLine, LineClassWhere.WRAP,
+ diffTable.style.activeLine());
+ other.addLineClass(oLine, LineClassWhere.BACKGROUND,
+ diffTable.style.activeLineBg());
+ }
+ }
+ };
+ }
+
+ private Runnable insertNewDraft(final CodeMirror cm) {
+ return new Runnable() {
+ public void run() {
+ Map<Integer, CommentBox> lineBoxMap = cm == cmA ?
+ lineBoxMapA : lineBoxMapB;
+ int line = cm.getActiveLine();
+ CommentBox box = lineBoxMap.get(line);
+ if (box == null) {
+ lineBoxMap.put(line, addNewDraft(cm, line));
+ } else if (box.isDraft()) {
+ ((DraftBox) lineBoxMap.get(line)).setEdit(true);
+ } else {
+ ((PublishedBox) box).onReply(null);
+ }
+ }
+ };
+ }
+
private static String getContentType(DiffInfo.FileMeta meta) {
return meta != null && meta.content_type() != null
? ModeInjector.getContentType(meta.content_type())
: null;
}
+ private static class LineWidgetElementPair {
+ private LineWidget widget;
+ private Element element;
+
+ private LineWidgetElementPair(LineWidget w, Element e) {
+ widget = w;
+ element = e;
+ }
+ }
+
static class EditIterator {
private final JsArrayString lines;
private final int startLine;
@@ -367,6 +532,9 @@
}
numOfChar -= lengthWithNewline;
advanceLine();
+ if (numOfChar == 0) {
+ return LineCharacter.create(startLine + currLineIndex, 0);
+ }
}
throw new IllegalStateException("EditIterator index out of bound");
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
index 879d16a..3ba3a0f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
@@ -14,11 +14,13 @@
package com.google.gerrit.client.diff;
-import com.google.gerrit.client.AvatarImage;
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gwt.core.client.GWT;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.shared.HandlerRegistration;
@@ -26,57 +28,54 @@
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import net.codemirror.lib.LineWidget;
import java.sql.Timestamp;
-/** An HtmlPanel holding the DialogBox to display a comment */
-class CommentBox extends Composite {
- interface Binder extends UiBinder<HTMLPanel, CommentBox> {}
- private static Binder uiBinder = GWT.create(Binder.class);
-
+/** An HtmlPanel for displaying a comment */
+abstract class CommentBox extends Composite {
interface CommentBoxStyle extends CssResource {
String open();
String close();
}
+ private CommentLinkProcessor commentLinkProcessor;
private HandlerRegistration headerClick;
- private Runnable clickCallback;
+ private CommentInfo original;
+ private PatchSet.Id patchSetId;
+ private LineWidget selfWidget;
+ private LineWidget paddingWidget;
+ private Element paddingWidgetEle;
+ private CodeMirrorDemo diffView;
+ private boolean draft;
+
+ @UiField(provided=true)
+ CommentBoxHeader header;
@UiField
- Widget header;
+ HTML contentPanelMessage;
@UiField
- AvatarImage avatar;
+ CommentBoxResources res;
- @UiField
- Element name;
-
- @UiField
- Element summary;
-
- @UiField
- Element date;
-
- @UiField
- Element contentPanel;
-
- @UiField
- Element contentPanelMessage;
-
- @UiField
- CommentBoxStyle style;
-
- CommentBox(AccountInfo author, Timestamp when, String message,
+ protected CommentBox(
+ CodeMirrorDemo host,
+ UiBinder<? extends Widget, CommentBox> binder,
+ PatchSet.Id id, CommentInfo info, CommentLinkProcessor linkProcessor,
boolean isDraft) {
- initWidget(uiBinder.createAndBindUi(this));
- // TODO: Format the comment box differently based on whether isDraft
- // is true.
- setAuthorNameText(author);
- date.setInnerText(FormatUtil.shortFormatDayTime(when));
- setMessageText(message);
- setOpen(false);
+ diffView = host;
+ commentLinkProcessor = linkProcessor;
+ original = info;
+ patchSetId = id;
+ draft = isDraft;
+ header = new CommentBoxHeader(info.author(), info.updated(), isDraft);
+ initWidget(binder.createAndBindUi(this));
+ setMessageText(info.message());
}
@Override
@@ -87,15 +86,9 @@
@Override
public void onClick(ClickEvent event) {
setOpen(!isOpen());
- if (clickCallback != null) {
- clickCallback.run();
- }
}
}, ClickEvent.getType());
- }
-
- void setOpenCloseHandler(final Runnable callback) {
- clickCallback = callback;
+ res.style().ensureInjected();
}
@Override
@@ -108,34 +101,82 @@
}
}
- private void setAuthorNameText(AccountInfo author) {
- // TODO: Set avatar's display to none if we get a 404.
- avatar = new AvatarImage(author, 26);
- name.setInnerText(FormatUtil.name(author));
+ void setSelfWidget(LineWidget widget) {
+ selfWidget = widget;
}
- private void setMessageText(String message) {
+ void setPadding(LineWidget widget, Element element) {
+ paddingWidget = widget;
+ paddingWidgetEle = element;
+ }
+
+ void resizePaddingWidget() {
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ paddingWidgetEle.getStyle().setHeight(getOffsetHeight(), Unit.PX);
+ paddingWidget.changed();
+ selfWidget.changed();
+ }
+ });
+ }
+
+ protected void setMessageText(String message) {
if (message == null) {
message = "";
} else {
message = message.trim();
}
- summary.setInnerText(message);
- // TODO: Change to use setInnerHtml
- contentPanelMessage.setInnerText(message);
+ header.setSummaryText(message);
+ SafeHtml buf = new SafeHtmlBuilder().append(message).wikify();
+ buf = commentLinkProcessor.apply(buf);
+ SafeHtml.set(contentPanelMessage, buf);
}
- private void setOpen(boolean open) {
+ protected void setDate(Timestamp when) {
+ header.setDate(when);
+ }
+
+ protected void setOpen(boolean open) {
if (open) {
- removeStyleName(style.close());
- addStyleName(style.open());
+ removeStyleName(res.style().close());
+ addStyleName(res.style().open());
} else {
- removeStyleName(style.open());
- addStyleName(style.close());
+ removeStyleName(res.style().open());
+ addStyleName(res.style().close());
}
+ resizePaddingWidget();
}
private boolean isOpen() {
- return getStyleName().contains(style.open());
+ return getStyleName().contains(res.style().open());
+ }
+
+ protected CodeMirrorDemo getDiffView() {
+ return diffView;
+ }
+
+ protected PatchSet.Id getPatchSetId() {
+ return patchSetId;
+ }
+
+ protected CommentInfo getOriginal() {
+ return original;
+ }
+
+ protected LineWidget getSelfWidget() {
+ return selfWidget;
+ }
+
+ protected LineWidget getPaddingWidget() {
+ return paddingWidget;
+ }
+
+ protected void updateOriginal(CommentInfo newInfo) {
+ original = newInfo;
+ }
+
+ boolean isDraft() {
+ return draft;
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.ui.xml
deleted file mode 100644
index 38dcd65..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.ui.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2013 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.
--->
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
- xmlns:g='urn:import:com.google.gwt.user.client.ui'
- xmlns:c='urn:import:com.google.gerrit.client'>
- <ui:style type='com.google.gerrit.client.diff.CommentBox.CommentBoxStyle'>
- .commentBox {
- background-color: #e5ecf9;
- border: 1px solid black;
- }
- .table {
- width: 100%;
- cursor: pointer;
- table-layout: fixed;
- }
- .summary {
- width: 60%;
- }
- .summaryText {
- color: #777;
- height: 1em;
- max-width: 80%;
- overflow: hidden;
- padding-bottom: 1px;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .open .summaryText {
- display: none;
- }
- .date {
- width: 25%;
- text-align: right;
- }
- .close .contentPanel {
- display: none;
- }
- .message {
- margin: 5px;
- }
- </ui:style>
- <g:HTMLPanel styleName='{style.commentBox}'>
- <g:HTMLPanel ui:field='header'>
- <table class='{style.table}'>
- <tr>
- <td><c:AvatarImage ui:field='avatar' /></td>
- <td ui:field='name'></td>
- <td class='{style.summary}'>
- <div ui:field='summary' class='{style.summaryText}'></div>
- </td>
- <td ui:field='date' class='{style.date}'></td>
- </tr>
- </table>
- </g:HTMLPanel>
- <div ui:field='contentPanel' class='{style.contentPanel}'>
- <div><p ui:field='contentPanelMessage' class='{style.message}'></p></div>
- <div>
- <button ui:field='reply'><ui:msg>Reply ...</ui:msg></button>
- <button ui:field='replyDone'><ui:msg>Reply 'Done'</ui:msg></button>
- </div>
- </div>
- </g:HTMLPanel>
-</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxHeader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxHeader.java
new file mode 100644
index 0000000..5ffd925
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxHeader.java
@@ -0,0 +1,79 @@
+//Copyright (C) 2013 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.client.diff;
+
+import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+
+import java.sql.Timestamp;
+
+/**
+ * An HtmlPanel representing the header of a CommentBox, displaying
+ * the author's avatar (if applicable), the author's name, the summary,
+ * and the date.
+ */
+class CommentBoxHeader extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, CommentBoxHeader> {}
+ private static Binder uiBinder = GWT.create(Binder.class);
+
+ private boolean draft;
+
+ @UiField(provided=true)
+ AvatarImage avatar;
+
+ @UiField
+ Element name;
+
+ @UiField
+ Element summary;
+
+ @UiField
+ Element date;
+
+ CommentBoxHeader(AccountInfo author, Timestamp when, boolean isDraft) {
+ // TODO: Set avatar's display to none if we get a 404.
+ avatar = author == null ? new AvatarImage() : new AvatarImage(author, 26);
+ initWidget(uiBinder.createAndBindUi(this));
+ this.draft = isDraft;
+ if (when != null) {
+ setDate(when);
+ }
+ if (isDraft) {
+ name.setInnerText("(Draft)");
+ } else {
+ name.setInnerText(FormatUtil.name(author));
+ }
+ }
+
+ void setDate(Timestamp when) {
+ if (draft) {
+ date.setInnerText(PatchUtil.M.draftSaved(when));
+ } else {
+ date.setInnerText(FormatUtil.shortFormatDayTime(when));
+ }
+ }
+
+ void setSummaryText(String message) {
+ summary.setInnerText(message);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxHeader.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxHeader.ui.xml
new file mode 100644
index 0000000..64ae22b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxHeader.ui.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:c='urn:import:com.google.gerrit.client'>
+ <ui:with field='res' type='com.google.gerrit.client.diff.CommentBoxResources' />
+ <g:HTMLPanel>
+ <table class='{res.style.table}'>
+ <tr>
+ <td><c:AvatarImage ui:field='avatar' /></td>
+ <td ui:field='name'></td>
+ <td class='{res.style.summary}'>
+ <div ui:field='summary' class='{res.style.summaryText}'></div>
+ </td>
+ <td ui:field='date' class='{res.style.date}'></td>
+ </tr>
+ </table>
+ </g:HTMLPanel>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxResources.java
new file mode 100644
index 0000000..e08eec5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxResources.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2013 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.client.diff;
+
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+
+/**
+ * Resources used by diff.
+ */
+interface CommentBoxResources extends ClientBundle {
+ @Source("CommentBoxUi.css")
+ Style style();
+
+ interface Style extends CssResource {
+ String open();
+ String close();
+ String commentBox();
+ String table();
+ String summary();
+ String summaryText();
+ String date();
+ String contentPanel();
+ String message();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
new file mode 100644
index 0000000..4af0157
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
@@ -0,0 +1,41 @@
+.commentBox {
+ background-color: #e5ecf9;
+ border: 1px solid black;
+}
+
+.table {
+ width: 100%;
+ cursor: pointer;
+ table-layout: fixed;
+}
+
+.summary {
+ width: 60%;
+}
+
+.summaryText {
+ color: #777;
+ height: 1em;
+ max-width: 80%;
+ overflow: hidden;
+ padding-bottom: 1px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.open .summaryText {
+ display: none;
+}
+
+.date {
+ width: 25%;
+ text-align: right;
+}
+
+.close .contentPanel {
+ display: none;
+}
+
+.message {
+ margin: 5px;
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
index ee556c5..da0a450 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -15,6 +15,7 @@
package com.google.gerrit.client.diff;
import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -24,6 +25,13 @@
NONE, TRAILING, CHANGED, ALL;
};
+ public static void list(int id, String revision,
+ AsyncCallback<NativeMap<FileInfo>> cb) {
+ ChangeApi.revision(id, revision)
+ .view("files")
+ .get(NativeMap.copyKeysIntoChildren("path", cb));
+ }
+
public static DiffApi diff(PatchSet.Id id, String path) {
return new DiffApi(ChangeApi.revision(id)
.view("files").id(path)
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index a177498..0e22876 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -35,6 +35,8 @@
String diff();
String intraline();
String padding();
+ String activeLine();
+ String activeLineBg();
}
@UiField
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
index 0e303bc..7608267 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
@@ -57,6 +57,12 @@
.a .CodeMirror-vscrollbar {
display: none !important;
}
+ .activeLine .CodeMirror-linenumber {
+ background-color: #D8EDF9;
+ }
+ .activeLineBg {
+ background-color: #D8EDF9;
+ }
</ui:style>
<g:HTMLPanel styleName='{style.difftable}'>
<table class='{style.table}'>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
new file mode 100644
index 0000000..c4906de
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
@@ -0,0 +1,201 @@
+//Copyright (C) 2013 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.client.diff;
+
+import com.google.gerrit.client.changes.CommentApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.changes.CommentInput;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+/** An HtmlPanel for displaying and editing a draft */
+class DraftBox extends CommentBox {
+ interface Binder extends UiBinder<HTMLPanel, DraftBox> {}
+ private static UiBinder<HTMLPanel, CommentBox> uiBinder =
+ GWT.create(Binder.class);
+
+ interface DraftBoxStyle extends CssResource {
+ String edit();
+ String view();
+ String newDraft();
+ }
+
+ @UiField
+ NpTextArea editArea;
+
+ @UiField
+ DraftBoxStyle draftStyle;
+
+ @UiField
+ Button edit;
+
+ @UiField
+ Button save;
+
+ @UiField
+ Button cancel;
+
+ @UiField
+ Button discard;
+
+ private HandlerRegistration messageClick;
+ private boolean isNew;
+ private PublishedBox replyToBox;
+
+ DraftBox(
+ CodeMirrorDemo host,
+ PatchSet.Id id,
+ CommentInfo info,
+ CommentLinkProcessor linkProcessor,
+ boolean isNew,
+ boolean saveOnInit) {
+ super(host, uiBinder, id, info, linkProcessor, true);
+
+ this.isNew = isNew;
+ editArea.setText(contentPanelMessage.getText());
+ if (saveOnInit) {
+ onSave(null);
+ }
+ if (isNew) {
+ addStyleName(draftStyle.newDraft());
+ }
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+
+ messageClick = contentPanelMessage.addDoubleClickHandler(
+ new DoubleClickHandler() {
+ @Override
+ public void onDoubleClick(DoubleClickEvent arg0) {
+ setEdit(true);
+ }
+ });
+ addDomHandler(new MouseMoveHandler() {
+ @Override
+ public void onMouseMove(MouseMoveEvent arg0) {
+ resizePaddingWidget();
+ }
+ }, MouseMoveEvent.getType());
+ }
+
+ @Override
+ protected void onUnload() {
+ super.onUnload();
+
+ messageClick.removeHandler();
+ messageClick = null;
+ }
+
+ void setEdit(boolean edit) {
+ if (edit) {
+ setOpen(true);
+ removeStyleName(draftStyle.view());
+ addStyleName(draftStyle.edit());
+ editArea.setText(contentPanelMessage.getText());
+ editArea.setFocus(true);
+ } else {
+ removeStyleName(draftStyle.edit());
+ addStyleName(draftStyle.view());
+ }
+ resizePaddingWidget();
+ }
+
+ void registerReplyToBox(PublishedBox box) {
+ replyToBox = box;
+ }
+
+ private void removeUI() {
+ if (replyToBox != null) {
+ replyToBox.unregisterReplyBox();
+ }
+ CommentInfo info = getOriginal();
+ getDiffView().removeCommentBox(info.side(), info.line() - 1);
+ removeFromParent();
+ getSelfWidget().clear();
+ getPaddingWidget().clear();
+ }
+
+ @UiHandler("edit")
+ void onEdit(ClickEvent e) {
+ setEdit(true);
+ }
+
+ @UiHandler("save")
+ void onSave(ClickEvent e) {
+ final String message = editArea.getText();
+ if (message.equals("")) {
+ return;
+ }
+ CommentInfo original = getOriginal();
+ CommentInput input = CommentInput.create(original);
+ input.setMessage(message);
+ GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
+ @Override
+ public void onSuccess(CommentInfo result) {
+ updateOriginal(result);
+ setMessageText(message);
+ setDate(result.updated());
+ setEdit(false);
+ if (isNew) {
+ removeStyleName(draftStyle.newDraft());
+ isNew = false;
+ }
+ }
+ };
+ if (isNew) {
+ CommentApi.createDraft(getPatchSetId(), input, cb);
+ } else {
+ CommentApi.updateDraft(getPatchSetId(), original.id(), input, cb);
+ }
+ }
+
+ @UiHandler("cancel")
+ void onCancel(ClickEvent e) {
+ setEdit(false);
+ }
+
+ @UiHandler("discard")
+ void onDiscard(ClickEvent e) {
+ if (isNew) {
+ removeUI();
+ } else {
+ CommentApi.deleteDraft(getPatchSetId(), getOriginal().id(),
+ new GerritCallback<JavaScriptObject>() {
+ @Override
+ public void onSuccess(JavaScriptObject result) {
+ removeUI();
+ }
+ });
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
new file mode 100644
index 0000000..d864406
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:d='urn:import:com.google.gerrit.client.diff'
+ xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+ <ui:with field='res' type='com.google.gerrit.client.diff.CommentBoxResources' />
+ <ui:style field='draftStyle' type='com.google.gerrit.client.diff.DraftBox.DraftBoxStyle'>
+ .edit .messagePanel {
+ display: none;
+ }
+ .view .editArea {
+ display: none;
+ }
+ .newDraft .cancel {
+ display: none;
+ }
+ </ui:style>
+ <g:HTMLPanel styleName='{res.style.commentBox}'>
+ <d:CommentBoxHeader ui:field='header' />
+ <div class='{res.style.contentPanel}'>
+ <c:NpTextArea ui:field='editArea' styleName='{draftStyle.editArea}'/>
+ <div>
+ <g:Button ui:field='save' styleName='{draftStyle.editArea}'>
+ <ui:msg>Save</ui:msg>
+ </g:Button>
+ <g:Button ui:field='cancel'
+ styleName='{draftStyle.editArea} {draftStyle.cancel}'>
+ <ui:msg>Cancel</ui:msg>
+ </g:Button>
+ <g:Button ui:field='discard' styleName='{draftStyle.editArea}'>
+ <ui:msg>Discard</ui:msg>
+ </g:Button>
+ </div>
+ </div>
+ <div class='{res.style.contentPanel}'>
+ <g:HTML ui:field='contentPanelMessage'
+ styleName='{res.style.message} {draftStyle.messagePanel}'></g:HTML>
+ <g:Button ui:field='edit' styleName='{draftStyle.messagePanel}'>
+ <ui:msg>Edit</ui:msg>
+ </g:Button>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
new file mode 100644
index 0000000..cc73a94
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 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.client.diff;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class FileInfo extends JavaScriptObject {
+ public final native String path() /*-{ return this.path; }-*/;
+ public final native String old_path() /*-{ return this.old_path; }-*/;
+ public final native int lines_inserted() /*-{ return this.lines_inserted || 0; }-*/;
+ public final native int lines_deleted() /*-{ return this.lines_deleted || 0; }-*/;
+ public final native boolean binary() /*-{ return this.binary || false; }-*/;
+
+ protected FileInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
index b86f59f..aed0813 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
@@ -49,7 +49,7 @@
int origLineB = lineB;
lineB += numLines;
int bAheadOfA = lineB - lineA;
- lineMapAtoB.add(new LineGap(lineA, lineA, bAheadOfA));
+ lineMapAtoB.add(new LineGap(lineA, -1, bAheadOfA));
lineMapBtoA.add(new LineGap(origLineB, lineB - 1, -bAheadOfA));
}
@@ -58,7 +58,7 @@
lineA += numLines;
int aAheadOfB = lineA - lineB;
lineMapAtoB.add(new LineGap(origLineA, lineA - 1, -aAheadOfB));
- lineMapBtoA.add(new LineGap(lineB, lineB, aAheadOfB));
+ lineMapBtoA.add(new LineGap(lineB, -1, aAheadOfB));
}
/**
@@ -69,8 +69,9 @@
*
* A LineGap records gap information from the start of an actual gap up to
* the start of the next gap. In the following example,
- * lineMapAtoB will have LineGap: {start: 1, end: 1, delta: 3}
- * (end doesn't really matter here, as the binary search only looks at start)
+ * lineMapAtoB will have LineGap: {start: 1, end: -1, delta: 3}
+ * (end set to -1 to represent a dummy gap of length zero. The binary search
+ * only looks at start so setting it to -1 has no effect here.)
* lineMapBtoA will have LineGap: {start: 1, end: 3, delta: -3}
* These LineGaps control lines between 1 and 5.
*
@@ -97,25 +98,62 @@
* - | 6
* ...
*/
- int lineOnOther(Side mySide, int line) {
+ LineOnOtherInfo lineOnOther(Side mySide, int line) {
List<LineGap> lineGaps = mySide == Side.PARENT ? lineMapAtoB : lineMapBtoA;
// Create a dummy LineGap for the search.
int ret = Collections.binarySearch(lineGaps, new LineGap(line));
if (ret == -1) {
- return line;
+ return new LineOnOtherInfo(line, true);
} else {
LineGap lookup = lineGaps.get(0 <= ret ? ret : -ret - 2);
+ int start = lookup.start;
int end = lookup.end;
int delta = lookup.delta;
- if (lookup.start <= line && line <= end) { // Line falls within gap
- return end + delta;
+ if (start <= line && line <= end && end != -1) { // Line falls within gap
+ return new LineOnOtherInfo(end + delta, false);
} else { // Line after gap
- return line + delta;
+ return new LineOnOtherInfo(line + delta, true);
}
}
}
/**
+ * @field line The line number on the other side.
+ * @field aligned Whether the two lines are at the same height when displayed.
+ */
+ static class LineOnOtherInfo {
+ private int line;
+ private boolean aligned;
+
+ LineOnOtherInfo(int line, boolean aligned) {
+ this.line = line;
+ this.aligned = aligned;
+ }
+
+ int getLine() {
+ return line;
+ }
+
+ boolean isAligned() {
+ return aligned;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof LineOnOtherInfo) {
+ LineOnOtherInfo other = (LineOnOtherInfo) obj;
+ return aligned == other.aligned && line == other.line;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return line + " " + aligned;
+ }
+ }
+
+ /**
* Helper class to record line gap info and assist in calculation of line
* number on the other side.
*
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
new file mode 100644
index 0000000..337b528
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
@@ -0,0 +1,72 @@
+//Copyright (C) 2013 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.client.diff;
+
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.HTMLPanel;
+
+/** An HtmlPanel for displaying a published comment */
+class PublishedBox extends CommentBox {
+ interface Binder extends UiBinder<HTMLPanel, PublishedBox> {}
+ private static UiBinder<HTMLPanel, CommentBox> uiBinder =
+ GWT.create(Binder.class);
+
+ private DraftBox replyBox;
+
+ PublishedBox(CodeMirrorDemo host, PatchSet.Id id, CommentInfo info,
+ CommentLinkProcessor linkProcessor) {
+ super(host, uiBinder, id, info, linkProcessor, false);
+ }
+
+ void registerReplyBox(DraftBox box) {
+ replyBox = box;
+ box.registerReplyToBox(this);
+ }
+
+ void unregisterReplyBox() {
+ replyBox = null;
+ }
+
+ private void openReplyBox() {
+ replyBox.setOpen(true);
+ replyBox.setEdit(true);
+ }
+
+ @UiHandler("reply")
+ void onReply(ClickEvent e) {
+ if (replyBox == null) {
+ DraftBox box = getDiffView().addReply(getOriginal(), "", false);
+ registerReplyBox(box);
+ } else {
+ openReplyBox();
+ }
+ }
+
+ @UiHandler("replyDone")
+ void onReplyDone(ClickEvent e) {
+ if (replyBox == null) {
+ DraftBox box = getDiffView().addReply(getOriginal(), "Done", true);
+ registerReplyBox(box);
+ } else {
+ openReplyBox();
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
new file mode 100644
index 0000000..a146e82
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:d='urn:import:com.google.gerrit.client.diff'>
+ <ui:with field='res' type='com.google.gerrit.client.diff.CommentBoxResources' />
+ <g:HTMLPanel styleName='{res.style.commentBox}'>
+ <d:CommentBoxHeader ui:field='header' />
+ <g:HTMLPanel styleName='{res.style.contentPanel}'>
+ <g:HTML ui:field='contentPanelMessage' styleName='{res.style.message}'></g:HTML>
+ <div>
+ <g:Button ui:field='reply'><ui:msg>Reply ...</ui:msg></g:Button>
+ <g:Button ui:field='replyDone'><ui:msg>Reply 'Done'</ui:msg></g:Button>
+ </div>
+ </g:HTMLPanel>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
index 3b4abe5..f529990 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
@@ -26,7 +26,7 @@
}
public final AccountGroup.UUID getGroupUUID() {
- return new AccountGroup.UUID(URL.decodePathSegment(id()));
+ return new AccountGroup.UUID(URL.decodeQueryString(id()));
}
public final native String id() /*-{ return this.id; }-*/;
@@ -46,13 +46,13 @@
public final AccountGroup.UUID getOwnerUUID() {
String owner = owner_id();
if (owner != null) {
- return new AccountGroup.UUID(URL.decodePathSegment(owner));
+ return new AccountGroup.UUID(URL.decodeQueryString(owner));
}
return null;
}
public final void setOwnerUUID(AccountGroup.UUID uuid) {
- owner_id(URL.encodePathSegment(uuid.get()));
+ owner_id(URL.encodeQueryString(uuid.get()));
}
protected GroupInfo() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
index d56eeaf..a971c51 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -80,6 +80,7 @@
}
public final native T get(String n) /*-{ return this[n]; }-*/;
+ public final native void put(String n, T v) /*-{ this[n] = v; }-*/;
public final native void copyKeysIntoChildren(String p)
/*-{
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
index afbd740..ce417bb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
@@ -195,7 +195,8 @@
final Element tr = DOM.getParent(fmt.getElement(currentRow, C_ARROW));
UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), false);
}
- if (newRow >= 0) {
+ if (0 <= newRow && newRow < table.getRowCount()
+ && getRowItem(newRow) != null) {
table.setWidget(newRow, C_ARROW, pointer);
final Element tr = DOM.getParent(fmt.getElement(newRow, C_ARROW));
UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), true);
@@ -255,7 +256,7 @@
}
@Override
- protected void resetHtml(SafeHtml body) {
+ public void resetHtml(SafeHtml body) {
currentRow = -1;
super.resetHtml(body);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
index a26db05..3f79a61 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
@@ -48,6 +48,7 @@
protected Screen() {
initWidget(new FlowPanel());
setStyleName(Gerrit.RESOURCES.css().screen());
+ body = new FlowPanel();
}
@Override
@@ -76,7 +77,7 @@
protected void onInitUI() {
final FlowPanel me = (FlowPanel) getWidget();
me.add(header = new Grid(1, Cols.values().length));
- me.add(body = new FlowPanel());
+ me.add(body);
headerText = new InlineLabel();
if (titleWidget == null) {
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index dbf1a2b..69d1671 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -66,6 +66,16 @@
this.addLineClass(line, where, lineClass);
}-*/;
+ public final void removeLineClass(int line, LineClassWhere where,
+ String className) {
+ removeLineClassNative(line, where.name().toLowerCase(), className);
+ }
+
+ private final native void removeLineClassNative(int line, String where,
+ String lineClass) /*-{
+ this.removeLineClass(line, where, lineClass);
+ }-*/;
+
public final native void addWidget(LineCharacter pos, Element node,
boolean scrollIntoView) /*-{
this.addWidget(pos, node, scrollIntoView);
@@ -102,6 +112,24 @@
});
}-*/;
+ public final native LineCharacter getCursor(String start) /*-{
+ return this.getCursor(start);
+ }-*/;
+
+ public final native boolean hasActiveLine() /*-{
+ return this.state.hasOwnProperty('activeLine');
+ }-*/;
+
+ public final native int getActiveLine() /*-{
+ return this.state.activeLine;
+ }-*/;
+
+ public final native void setActiveLine(int line) /*-{
+ this.state.activeLine = line;
+ }-*/;
+
+ public final native void addKeyMap(KeyMap map) /*-{ this.addKeyMap(map); }-*/;
+
protected CodeMirror() {
}
}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
new file mode 100644
index 0000000..ac2ca5c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** Object that associates a key or key combination with a handler. */
+public class KeyMap extends JavaScriptObject {
+ public static KeyMap create() {
+ return createObject().cast();
+ }
+
+ public final native KeyMap on(String key, Runnable thunk) /*-{
+ this[key] = function() { $entry(thunk.@java.lang.Runnable::run()()); };
+ return this;
+ }-*/;
+
+ protected KeyMap() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java
index 9a7b64a..c1e5469 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java
@@ -25,8 +25,8 @@
return lineCh;
}
- public final native void setLine(int line) /*-{ this.line = line; }-*/;
- public final native void setCh(int ch) /*-{ this.ch = ch; }-*/;
+ private final native void setLine(int line) /*-{ this.line = line; }-*/;
+ private final native void setCh(int ch) /*-{ this.ch = ch; }-*/;
public final native int getLine() /*-{ return this.line; }-*/;
public final native int getCh() /*-{ return this.ch; }-*/;
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
index 74405e9..96ba2b7 100644
--- a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
@@ -16,6 +16,7 @@
import static org.junit.Assert.assertEquals;
+import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
import com.google.gerrit.common.changes.Side;
import org.junit.Test;
@@ -51,41 +52,55 @@
public void testFindInCommon() {
LineMapper mapper = new LineMapper();
mapper.appendCommon(10);
- assertEquals(9, mapper.lineOnOther(Side.PARENT, 9));
+ assertEquals(new LineOnOtherInfo(9, true),
+ mapper.lineOnOther(Side.PARENT, 9));
+ assertEquals(new LineOnOtherInfo(9, true),
+ mapper.lineOnOther(Side.REVISION, 9));
}
@Test
public void testFindAfterCommon() {
LineMapper mapper = new LineMapper();
mapper.appendCommon(10);
- assertEquals(10, mapper.lineOnOther(Side.PARENT, 10));
+ assertEquals(new LineOnOtherInfo(10, true),
+ mapper.lineOnOther(Side.PARENT, 10));
+ assertEquals(new LineOnOtherInfo(10, true),
+ mapper.lineOnOther(Side.REVISION, 10));
}
@Test
public void testFindInInsertGap() {
LineMapper mapper = new LineMapper();
mapper.appendInsert(10);
- assertEquals(-1, mapper.lineOnOther(Side.REVISION, 9));
+ assertEquals(new LineOnOtherInfo(-1, false),
+ mapper.lineOnOther(Side.REVISION, 9));
}
@Test
public void testFindAfterInsertGap() {
LineMapper mapper = new LineMapper();
mapper.appendInsert(10);
- assertEquals(0, mapper.lineOnOther(Side.REVISION, 10));
+ assertEquals(new LineOnOtherInfo(0, true),
+ mapper.lineOnOther(Side.REVISION, 10));
+ assertEquals(new LineOnOtherInfo(10, true),
+ mapper.lineOnOther(Side.PARENT, 0));
}
@Test
public void testFindInDeleteGap() {
LineMapper mapper = new LineMapper();
mapper.appendDelete(10);
- assertEquals(-1, mapper.lineOnOther(Side.PARENT, 9));
+ assertEquals(new LineOnOtherInfo(-1, false),
+ mapper.lineOnOther(Side.PARENT, 9));
}
@Test
public void testFindAfterDeleteGap() {
LineMapper mapper = new LineMapper();
mapper.appendDelete(10);
- assertEquals(0, mapper.lineOnOther(Side.PARENT, 10));
+ assertEquals(new LineOnOtherInfo(0, true),
+ mapper.lineOnOther(Side.PARENT, 10));
+ assertEquals(new LineOnOtherInfo(10, true),
+ mapper.lineOnOther(Side.REVISION, 0));
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index ff65674..e0bef35 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -77,7 +77,7 @@
String runas = req.getHeader(RUN_AS);
if (runas != null) {
if (!enabled) {
- RestApiServlet.replyError(res,
+ RestApiServlet.replyError(req, res,
SC_FORBIDDEN,
RUN_AS + " disabled by auth.enableRunAs = false");
return;
@@ -85,7 +85,7 @@
CurrentUser self = session.get().getCurrentUser();
if (!self.getCapabilities().canRunAs()) {
- RestApiServlet.replyError(res,
+ RestApiServlet.replyError(req, res,
SC_FORBIDDEN,
"not permitted to use " + RUN_AS);
return;
@@ -96,13 +96,13 @@
target = accountResolver.find(runas);
} catch (OrmException e) {
log.warn("cannot resolve account for " + RUN_AS, e);
- RestApiServlet.replyError(res,
+ RestApiServlet.replyError(req, res,
SC_INTERNAL_SERVER_ERROR,
"cannot resolve " + RUN_AS);
return;
}
if (target == null) {
- RestApiServlet.replyError(res,
+ RestApiServlet.replyError(req, res,
SC_FORBIDDEN,
"no account matches " + RUN_AS);
return;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 371e9ea4..9183d5c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -68,7 +68,7 @@
clp.parseOptionMap(in);
} catch (CmdLineException e) {
if (!clp.wasHelpRequestedByOption()) {
- replyError(res, SC_BAD_REQUEST, e.getMessage());
+ replyError(req, res, SC_BAD_REQUEST, e.getMessage());
return false;
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index fac950b..c671ec7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -52,6 +52,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -112,7 +113,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
@@ -300,9 +300,9 @@
if (result instanceof Response) {
@SuppressWarnings("rawtypes")
- Response r = (Response) result;
+ Response<?> r = (Response) result;
status = r.statusCode();
- configureCaching(req, res, r);
+ configureCaching(req, res, r.caching());
} else if (result instanceof Response.Redirect) {
CacheHeaders.setNotCacheable(res);
res.sendRedirect(((Response.Redirect) result).location());
@@ -321,27 +321,27 @@
}
}
} catch (AuthException e) {
- replyError(res, status = SC_FORBIDDEN, e.getMessage());
+ replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching());
} catch (BadRequestException e) {
- replyError(res, status = SC_BAD_REQUEST, e.getMessage());
+ replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), e.caching());
} catch (MethodNotAllowedException e) {
- replyError(res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed");
+ replyError(req, res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed", e.caching());
} catch (ResourceConflictException e) {
- replyError(res, status = SC_CONFLICT, e.getMessage());
+ replyError(req, res, status = SC_CONFLICT, e.getMessage(), e.caching());
} catch (PreconditionFailedException e) {
- replyError(res, status = SC_PRECONDITION_FAILED,
- Objects.firstNonNull(e.getMessage(), "Precondition failed"));
+ replyError(req, res, status = SC_PRECONDITION_FAILED,
+ Objects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching());
} catch (ResourceNotFoundException e) {
- replyError(res, status = SC_NOT_FOUND, "Not found");
+ replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching());
} catch (UnprocessableEntityException e) {
- replyError(res, status = 422,
- Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"));
+ replyError(req, res, status = 422,
+ Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching());
} catch (AmbiguousViewException e) {
- replyError(res, status = SC_NOT_FOUND, e.getMessage());
+ replyError(req, res, status = SC_NOT_FOUND, e.getMessage());
} catch (MalformedJsonException e) {
- replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+ replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
} catch (JsonParseException e) {
- replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+ replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
} catch (Exception e) {
status = SC_INTERNAL_SERVER_ERROR;
handleException(e, req, res);
@@ -354,18 +354,18 @@
}
private static <T> void configureCaching(HttpServletRequest req,
- HttpServletResponse res, Response<T> r) {
+ HttpServletResponse res, CacheControl c) {
if ("GET".equals(req.getMethod())) {
- switch (r.caching()) {
+ switch (c.getType()) {
case NONE:
default:
CacheHeaders.setNotCacheable(res);
break;
case PRIVATE:
- CacheHeaders.setCacheablePrivate(res, 7, TimeUnit.DAYS);
+ CacheHeaders.setCacheablePrivate(res, c.getAge(), c.getUnit());
break;
case PUBLIC:
- CacheHeaders.setCacheable(req, res, 7, TimeUnit.DAYS);
+ CacheHeaders.setCacheable(req, res, c.getAge(), c.getUnit());
break;
}
} else {
@@ -837,14 +837,20 @@
if (!res.isCommitted()) {
res.reset();
- replyError(res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
+ replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
}
}
- public static void replyError(HttpServletResponse res, int statusCode,
- String msg) throws IOException {
+ public static void replyError(HttpServletRequest req,
+ HttpServletResponse res, int statusCode, String msg) throws IOException {
+ replyError(req, res, statusCode, msg, CacheControl.NONE);
+ }
+
+ public static void replyError(HttpServletRequest req,
+ HttpServletResponse res, int statusCode, String msg,
+ CacheControl c) throws IOException {
res.setStatus(statusCode);
- CacheHeaders.setNotCacheable(res);
+ configureCaching(req, res, c);
replyText(null, res, msg);
}
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
index 67dd218..b5e5d07 100644
--- a/gerrit-lucene/BUCK
+++ b/gerrit-lucene/BUCK
@@ -10,6 +10,7 @@
'//gerrit-reviewdb:server',
'//gerrit-server:server',
'//lib:gwtorm',
+ '//lib:guava',
'//lib/lucene:core',
],
visibility = ['PUBLIC'],
@@ -26,7 +27,9 @@
'//gerrit-server:server',
'//lib:guava',
'//lib:gwtorm',
+ '//lib:jsr305',
'//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
'//lib/jgit:jgit',
'//lib/log:api',
'//lib/lucene:analyzers-common',
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/IndexVersionCheck.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/IndexVersionCheck.java
deleted file mode 100644
index 6060312..0000000
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/IndexVersionCheck.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2013 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.lucene;
-
-import static com.google.gerrit.lucene.LuceneChangeIndex.LUCENE_VERSION;
-
-import static org.apache.lucene.util.Version.LUCENE_CURRENT;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.ChangeField;
-import com.google.inject.Inject;
-import com.google.inject.ProvisionException;
-
-import org.apache.lucene.util.Version;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Map;
-
-public class IndexVersionCheck implements LifecycleListener {
- public static final Map<String, Integer> SCHEMA_VERSIONS = ImmutableMap.of(
- LuceneChangeIndex.CHANGES_OPEN, ChangeField.SCHEMA_VERSION,
- LuceneChangeIndex.CHANGES_CLOSED, ChangeField.SCHEMA_VERSION);
-
- public static File gerritIndexConfig(SitePaths sitePaths) {
- return new File(sitePaths.index_dir, "gerrit_index.config");
- }
-
- private final SitePaths sitePaths;
-
- @Inject
- IndexVersionCheck(SitePaths sitePaths) {
- this.sitePaths = sitePaths;
- }
-
- @Override
- public void start() {
- File file = gerritIndexConfig(sitePaths);
- try {
- FileBasedConfig cfg = new FileBasedConfig(file, FS.detect());
- cfg.load();
- for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
- int schemaVersion = cfg.getInt("index", e.getKey(), "schemaVersion", 0);
- if (schemaVersion != e.getValue()) {
- throw new ProvisionException(String.format(
- "wrong index schema version for \"%s\": expected %d, found %d%s",
- e.getKey(), e.getValue(), schemaVersion, upgrade()));
- }
- }
- @SuppressWarnings("deprecation")
- Version luceneVersion =
- cfg.getEnum("lucene", null, "version", LUCENE_CURRENT);
- if (luceneVersion != LUCENE_VERSION) {
- throw new ProvisionException(String.format(
- "wrong Lucene version: expected %d, found %d%s",
- luceneVersion, LUCENE_VERSION, upgrade()));
-
- }
- } catch (IOException e) {
- throw new ProvisionException("unable to read " + file);
- } catch (ConfigInvalidException e) {
- throw new ProvisionException("invalid config file " + file);
- }
- }
-
- @Override
- public void stop() {
- // Do nothing.
- }
-
- private final String upgrade() {
- return "\nRun reindex to rebuild the index:\n"
- + "$ java -jar gerrit.war reindex -d "
- + sitePaths.site_path.getAbsolutePath();
- }
-}
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 5e8cac5..2494e2c 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
@@ -14,10 +14,8 @@
package com.google.gerrit.lucene;
-import static com.google.gerrit.lucene.IndexVersionCheck.SCHEMA_VERSIONS;
-import static com.google.gerrit.lucene.IndexVersionCheck.gerritIndexConfig;
-import static com.google.gerrit.server.query.change.IndexRewriteImpl.CLOSED_STATUSES;
-import static com.google.gerrit.server.query.change.IndexRewriteImpl.OPEN_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
@@ -26,21 +24,25 @@
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
-import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.ChangeIndex;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldDef.FillArgs;
import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.IndexRewriteImpl;
+import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gerrit.server.query.change.IndexRewriteImpl;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
@@ -65,7 +67,6 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -75,11 +76,12 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
+import javax.annotation.Nullable;
+
/**
* Secondary index implementation using Apache Lucene.
* <p>
@@ -88,15 +90,19 @@
* though there may be some lag between a committed write and it showing up to
* other threads' searchers.
*/
-public class LuceneChangeIndex implements ChangeIndex, LifecycleListener {
+public class LuceneChangeIndex implements ChangeIndex {
private static final Logger log =
LoggerFactory.getLogger(LuceneChangeIndex.class);
public static final Version LUCENE_VERSION = Version.LUCENE_43;
- public static final String CHANGES_OPEN = "changes_open";
- public static final String CHANGES_CLOSED = "changes_closed";
+ public static final String CHANGES_OPEN = "open";
+ public static final String CHANGES_CLOSED = "closed";
private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+ static interface Factory {
+ LuceneChangeIndex create(Schema<ChangeData> schema, String base);
+ }
+
private static IndexWriterConfig getIndexWriterConfig(Config cfg, String name) {
IndexWriterConfig writerConfig = new IndexWriterConfig(LUCENE_VERSION,
new StandardAnalyzer(LUCENE_VERSION));
@@ -112,29 +118,37 @@
private final SitePaths sitePaths;
private final FillArgs fillArgs;
private final ExecutorService executor;
- private final boolean readOnly;
+ private final File dir;
+ private final Schema<ChangeData> schema;
private final SubIndex openIndex;
private final SubIndex closedIndex;
- LuceneChangeIndex(Config cfg, SitePaths sitePaths,
- ListeningScheduledExecutorService executor, FillArgs fillArgs,
- boolean readOnly) throws IOException {
+ @AssistedInject
+ LuceneChangeIndex(
+ @GerritServerConfig Config cfg,
+ SitePaths sitePaths,
+ @IndexExecutor ListeningScheduledExecutorService executor,
+ FillArgs fillArgs,
+ @Assisted Schema<ChangeData> schema,
+ @Assisted @Nullable String base) throws IOException {
this.sitePaths = sitePaths;
this.fillArgs = fillArgs;
this.executor = executor;
- this.readOnly = readOnly;
- openIndex = new SubIndex(new File(sitePaths.index_dir, CHANGES_OPEN),
+ this.schema = schema;
+
+ if (base == null) {
+ dir = LuceneVersionManager.getDir(sitePaths, schema);
+ } else {
+ dir = new File(base);
+ }
+ openIndex = new SubIndex(new File(dir, CHANGES_OPEN),
getIndexWriterConfig(cfg, "changes_open"));
- closedIndex = new SubIndex(new File(sitePaths.index_dir, CHANGES_CLOSED),
- getIndexWriterConfig(cfg, "changes_closed"));
+ closedIndex = new SubIndex(new File(dir, CHANGES_CLOSED),
+ getIndexWriterConfig(cfg, "changes_closed"));
}
@Override
- public void start() {
- }
-
- @Override
- public void stop() {
+ public void close() {
List<Future<?>> closeFutures = Lists.newArrayListWithCapacity(2);
closeFutures.add(executor.submit(new Runnable() {
@Override
@@ -153,15 +167,16 @@
}
}
+ @Override
+ public Schema<ChangeData> getSchema() {
+ return schema;
+ }
+
@SuppressWarnings("unchecked")
@Override
public ListenableFuture<Void> insert(ChangeData cd) throws IOException {
Term id = QueryBuilder.idTerm(cd);
Document doc = toDocument(cd);
- if (readOnly) {
- return Futures.immediateFuture(null);
- }
-
if (cd.getChange().getStatus().isOpen()) {
return allOf(
closedIndex.delete(id),
@@ -178,9 +193,6 @@
public ListenableFuture<Void> replace(ChangeData cd) throws IOException {
Term id = QueryBuilder.idTerm(cd);
Document doc = toDocument(cd);
- if (readOnly) {
- return Futures.immediateFuture(null);
- }
if (cd.getChange().getStatus().isOpen()) {
return allOf(
closedIndex.delete(id),
@@ -196,9 +208,6 @@
@Override
public ListenableFuture<Void> delete(ChangeData cd) throws IOException {
Term id = QueryBuilder.idTerm(cd);
- if (readOnly) {
- return Futures.immediateFuture(null);
- }
return allOf(
openIndex.delete(id),
closedIndex.delete(id));
@@ -235,6 +244,17 @@
return new QuerySource(indexes, QueryBuilder.toQuery(p));
}
+ @Override
+ public void markReady(boolean ready) throws IOException {
+ try {
+ FileBasedConfig cfg = LuceneVersionManager.loadGerritIndexConfig(sitePaths);
+ LuceneVersionManager.setReady(cfg, schema.getVersion(), ready);
+ cfg.save();
+ } catch (ConfigInvalidException e) {
+ throw new IOException(e);
+ }
+ }
+
private static class QuerySource implements ChangeDataSource {
// TODO(dborowitz): Push limit down from predicate tree.
private static final int LIMIT = 1000;
@@ -259,6 +279,11 @@
}
@Override
+ public String toString() {
+ return query.toString();
+ }
+
+ @Override
public ResultSet<ChangeData> read() throws OrmException {
IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
Sort sort = new Sort(
@@ -318,7 +343,7 @@
private Document toDocument(ChangeData cd) throws IOException {
try {
Document result = new Document();
- for (FieldDef<ChangeData, ?> f : ChangeField.ALL.values()) {
+ for (FieldDef<ChangeData, ?> f : schema.getFields().values()) {
if (f.isRepeatable()) {
add(result, f, (Iterable<?>) f.get(cd, fillArgs));
} else {
@@ -369,17 +394,4 @@
private static Field.Store store(FieldDef<?, ?> f) {
return f.isStored() ? Field.Store.YES : Field.Store.NO;
}
-
- @Override
- public void finishIndex() throws IOException,
- ConfigInvalidException {
- FileBasedConfig cfg =
- new FileBasedConfig(gerritIndexConfig(sitePaths), FS.detect());
-
- for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
- cfg.setInt("index", e.getKey(), "schemaVersion", e.getValue());
- }
- cfg.setEnum("lucene", null, "version", LUCENE_VERSION);
- cfg.save();
- }
}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 3664d82..4e8f3f9 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -14,53 +14,102 @@
package com.google.gerrit.lucene;
-import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.FieldDef.FillArgs;
-import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
import com.google.inject.Provides;
import com.google.inject.Singleton;
-import org.eclipse.jgit.lib.Config;
-
-import java.io.IOException;
-
public class LuceneIndexModule extends LifecycleModule {
- private final boolean checkVersion;
+ private final Integer singleVersion;
private final int threads;
- private final boolean readOnly;
+ private final String base;
public LuceneIndexModule() {
- this(true, 0, false);
+ this(null, 0, null);
}
- public LuceneIndexModule(boolean checkVersion, int threads,
- boolean readOnly) {
- this.checkVersion = checkVersion;
+ public LuceneIndexModule(Integer singleVersion, int threads,
+ String base) {
+ this.singleVersion = singleVersion;
this.threads = threads;
- this.readOnly = readOnly;
+ this.base = base;
}
@Override
protected void configure() {
+ install(new FactoryModule() {
+ @Override
+ public void configure() {
+ factory(LuceneChangeIndex.Factory.class);
+ }
+ });
install(new IndexModule(threads));
- bind(ChangeIndex.class).to(LuceneChangeIndex.class);
- listener().to(LuceneChangeIndex.class);
- if (checkVersion) {
- listener().to(IndexVersionCheck.class);
+ if (singleVersion == null && base == null) {
+ install(new MultiVersionModule());
+ } else {
+ install(new SingleVersionModule());
}
}
- @Provides
+ private class MultiVersionModule extends LifecycleModule {
+ @Override
+ public void configure() {
+ install(new FactoryModule() {
+ @Override
+ public void configure() {
+ factory(OnlineReindexer.Factory.class);
+ }
+ });
+ listener().to(LuceneVersionManager.class);
+ }
+ }
+
+ private class SingleVersionModule extends LifecycleModule {
+ @Override
+ public void configure() {
+ listener().to(SingleVersionListener.class);
+ }
+
+ @Provides
+ @Singleton
+ LuceneChangeIndex getIndex(LuceneChangeIndex.Factory factory,
+ SitePaths sitePaths) {
+ Schema<ChangeData> schema = singleVersion != null
+ ? ChangeSchemas.get(singleVersion)
+ : ChangeSchemas.getLatest();
+ return factory.create(schema, base);
+ }
+ }
+
@Singleton
- public LuceneChangeIndex getChangeIndex(@GerritServerConfig Config cfg,
- SitePaths sitePaths,
- @IndexExecutor ListeningScheduledExecutorService executor,
- FillArgs fillArgs) throws IOException {
- return new LuceneChangeIndex(cfg, sitePaths, executor, fillArgs, readOnly);
+ static class SingleVersionListener implements LifecycleListener {
+ private final IndexCollection indexes;
+ private final LuceneChangeIndex index;
+
+ @Inject
+ SingleVersionListener(IndexCollection indexes,
+ LuceneChangeIndex index) {
+ this.indexes = indexes;
+ this.index = index;
+ }
+
+ @Override
+ public void start() {
+ indexes.setSearchIndex(index);
+ indexes.addWriteIndex(index);
+ }
+
+ @Override
+ public void stop() {
+ index.close();
+ }
}
}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
new file mode 100644
index 0000000..91dd015
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -0,0 +1,217 @@
+// Copyright (C) 2013 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.git;
+
+package com.google.gerrit.lucene;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.TreeMap;
+
+@Singleton
+class LuceneVersionManager implements LifecycleListener {
+ private static final Logger log = LoggerFactory
+ .getLogger(LuceneVersionManager.class);
+
+ private static final String CHANGES_PREFIX = "changes_";
+
+ private static class Version {
+ private final Schema<ChangeData> schema;
+ private final int version;
+ private final boolean exists;
+ private final boolean ready;
+
+ private Version(Schema<ChangeData> schema, int version, boolean exists,
+ boolean ready) {
+ checkArgument(schema == null || schema.getVersion() == version);
+ this.schema = schema;
+ this.version = version;
+ this.exists = exists;
+ this.ready = ready;
+ }
+ }
+
+ static File getDir(SitePaths sitePaths, Schema<ChangeData> schema) {
+ return new File(sitePaths.index_dir, String.format("%s%04d",
+ CHANGES_PREFIX, schema.getVersion()));
+ }
+
+ static FileBasedConfig loadGerritIndexConfig(SitePaths sitePaths)
+ throws ConfigInvalidException, IOException {
+ FileBasedConfig cfg = new FileBasedConfig(
+ new File(sitePaths.index_dir, "gerrit_index.config"), FS.detect());
+ cfg.load();
+ return cfg;
+ }
+
+ static void setReady(Config cfg, int version, boolean ready) {
+ cfg.setBoolean("index", Integer.toString(version), "ready", ready);
+ }
+
+ private static boolean getReady(Config cfg, int version) {
+ return cfg.getBoolean("index", Integer.toString(version), "ready", false);
+ }
+
+ private final SitePaths sitePaths;
+ private final LuceneChangeIndex.Factory indexFactory;
+ private final IndexCollection indexes;
+ private final OnlineReindexer.Factory reindexerFactory;
+
+ @Inject
+ LuceneVersionManager(
+ SitePaths sitePaths,
+ LuceneChangeIndex.Factory indexFactory,
+ IndexCollection indexes,
+ OnlineReindexer.Factory reindexerFactory) {
+ this.sitePaths = sitePaths;
+ this.indexFactory = indexFactory;
+ this.indexes = indexes;
+ this.reindexerFactory = reindexerFactory;
+ }
+
+ @Override
+ public void start() {
+ FileBasedConfig cfg;
+ try {
+ cfg = loadGerritIndexConfig(sitePaths);
+ } catch (ConfigInvalidException e) {
+ throw fail(e);
+ } catch (IOException e) {
+ throw fail(e);
+ }
+
+ TreeMap<Integer, Version> versions = scanVersions(cfg);
+ // Search from the most recent ready version.
+ // Write to the most recent ready version and the most recent version.
+ Version search = null;
+ List<Version> write = Lists.newArrayListWithCapacity(2);
+ for (Version v : versions.descendingMap().values()) {
+ if (v.schema == null) {
+ continue;
+ }
+ if (write.isEmpty()) {
+ write.add(v);
+ }
+ if (v.ready) {
+ search = v;
+ if (!write.contains(v)) {
+ write.add(v);
+ }
+ break;
+ }
+ }
+ if (search == null) {
+ throw new ProvisionException("No index versions ready; run Reindex");
+ }
+
+ markNotReady(cfg, versions.values(), write);
+ LuceneChangeIndex searchIndex = indexFactory.create(search.schema, null);
+ indexes.setSearchIndex(searchIndex);
+ for (Version v : write) {
+ if (v.schema != null) {
+ if (v.version != search.version) {
+ indexes.addWriteIndex(indexFactory.create(v.schema, null));
+ } else {
+ indexes.addWriteIndex(searchIndex);
+ }
+ }
+ }
+
+ int latest = write.get(0).version;
+ if (latest != search.version) {
+ reindexerFactory.create(latest).start();
+ }
+ }
+
+ private TreeMap<Integer, Version> scanVersions(Config cfg) {
+ TreeMap<Integer, Version> versions = Maps.newTreeMap();
+ for (Schema<ChangeData> schema : ChangeSchemas.ALL.values()) {
+ File f = getDir(sitePaths, schema);
+ boolean exists = f.exists() && f.isDirectory();
+ if (exists && !f.isDirectory()) {
+ log.warn("Not a directory: %s", f.getAbsolutePath());
+ }
+ int v = schema.getVersion();
+ versions.put(v, new Version(schema, v, exists, getReady(cfg, v)));
+ }
+
+ for (File f : sitePaths.index_dir.listFiles()) {
+ if (!f.getName().startsWith(CHANGES_PREFIX)) {
+ continue;
+ }
+ String versionStr = f.getName().substring(CHANGES_PREFIX.length());
+ Integer v = Ints.tryParse(versionStr);
+ if (v == null || versionStr.length() != 4) {
+ log.warn("Unrecognized version in index directory: {}",
+ f.getAbsolutePath());
+ continue;
+ }
+ if (!versions.containsKey(v)) {
+ versions.put(v, new Version(null, v, true, getReady(cfg, v)));
+ }
+ }
+ return versions;
+ }
+
+ private void markNotReady(FileBasedConfig cfg, Iterable<Version> versions,
+ Collection<Version> inUse) {
+ boolean dirty = false;
+ for (Version v : versions) {
+ if (!inUse.contains(v) && v.exists) {
+ setReady(cfg, v.version, false);
+ dirty = true;
+ }
+ }
+ if (dirty) {
+ try {
+ cfg.save();
+ } catch (IOException e) {
+ throw fail(e);
+ }
+ }
+ }
+
+ private ProvisionException fail(Throwable t) {
+ ProvisionException e = new ProvisionException("Error scanning indexes");
+ e.initCause(t);
+ throw e;
+ }
+
+ @Override
+ public void stop() {
+ }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
new file mode 100644
index 0000000..08338eb
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 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.git;
+
+package com.google.gerrit.lucene;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.index.ChangeBatchIndexer;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+public class OnlineReindexer {
+ private static final Logger log = LoggerFactory
+ .getLogger(OnlineReindexer.class);
+
+ public interface Factory {
+ OnlineReindexer create(int version);
+ }
+
+ private final IndexCollection indexes;
+ private final ChangeBatchIndexer batchIndexer;
+ private final ProjectCache projectCache;
+ private final int version;
+
+ @Inject
+ OnlineReindexer(
+ IndexCollection indexes,
+ ChangeBatchIndexer batchIndexer,
+ ProjectCache projectCache,
+ @Assisted int version) {
+ this.indexes = indexes;
+ this.batchIndexer = batchIndexer;
+ this.projectCache = projectCache;
+ this.version = version;
+ }
+
+ public void start() {
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ reindex();
+ }
+ };
+ t.setName(String.format("Reindex v%d-v%d",
+ version(indexes.getSearchIndex()), version));
+ t.start();
+ }
+
+ private static int version(ChangeIndex i) {
+ return i.getSchema().getVersion();
+ }
+
+ private void reindex() {
+ ChangeIndex index = checkNotNull(indexes.getWriteIndex(version),
+ "not an active write schema version: %s", version);
+ log.info("Starting online reindex from schema version {} to {}",
+ version(indexes.getSearchIndex()), version(index));
+ ChangeBatchIndexer.Result result = batchIndexer.indexAll(
+ index, projectCache.all(), -1, -1, null, null);
+ if (!result.success()) {
+ log.error("Online reindex of schema version {} failed", version(index));
+ return;
+ }
+
+ indexes.setSearchIndex(index);
+ log.info("Reindex complete, using schema version {}", version(index));
+ try {
+ index.markReady(true);
+ } catch (IOException e) {
+ log.warn("Error activating new schema version {}", version(index));
+ }
+
+ List<ChangeIndex> toRemove = Lists.newArrayListWithExpectedSize(1);
+ for (ChangeIndex i : indexes.getWriteIndexes()) {
+ if (version(i) != version(index)) {
+ toRemove.add(i);
+ }
+ }
+ for (ChangeIndex i : toRemove) {
+ try {
+ i.markReady(false);
+ indexes.removeWriteIndex(version(i));
+ } catch (IOException e) {
+ log.warn("Error deactivating old schema version {}", version(i));
+ }
+ }
+ }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
index e1c08d2..f491bc2 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -18,6 +18,7 @@
import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT;
import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
+import com.google.common.collect.Lists;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.IndexPredicate;
@@ -32,9 +33,9 @@
import com.google.gerrit.server.query.change.SortKeyPredicate;
import org.apache.lucene.index.Term;
-import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.FuzzyQuery;
+import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
@@ -44,6 +45,7 @@
import org.apache.lucene.util.NumericUtils;
import java.sql.Timestamp;
+import java.util.List;
public class QueryBuilder {
private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
@@ -55,28 +57,66 @@
public static Query toQuery(Predicate<ChangeData> p)
throws QueryParseException {
if (p instanceof AndPredicate) {
- return booleanQuery(p, MUST);
+ return and(p);
} else if (p instanceof OrPredicate) {
- return booleanQuery(p, SHOULD);
+ return or(p);
} else if (p instanceof NotPredicate) {
- if (p.getChild(0) instanceof TimestampRangePredicate) {
- return notTimestampQuery(
- (TimestampRangePredicate<ChangeData>) p.getChild(0));
- }
- return booleanQuery(p, MUST_NOT);
+ return not(p);
} else if (p instanceof IndexPredicate) {
return fieldQuery((IndexPredicate<ChangeData>) p);
} else {
- throw new QueryParseException("Cannot convert to index predicate: " + p);
+ throw new QueryParseException("cannot create query for index: " + p);
}
}
- private static Query booleanQuery(Predicate<ChangeData> p, BooleanClause.Occur o)
- throws QueryParseException {
- BooleanQuery q = new BooleanQuery();
- for (int i = 0; i < p.getChildCount(); i++) {
- q.add(toQuery(p.getChild(i)), o);
+ private static Query or(Predicate<ChangeData> p) throws QueryParseException {
+ try {
+ BooleanQuery q = new BooleanQuery();
+ for (int i = 0; i < p.getChildCount(); i++) {
+ q.add(toQuery(p.getChild(i)), SHOULD);
+ }
+ return q;
+ } catch (BooleanQuery.TooManyClauses e) {
+ throw new QueryParseException("cannot create query for index: " + p, e);
}
+ }
+
+ private static Query and(Predicate<ChangeData> p) throws QueryParseException {
+ try {
+ BooleanQuery b = new BooleanQuery();
+ List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount());
+ for (int i = 0; i < p.getChildCount(); i++) {
+ Predicate<ChangeData> c = p.getChild(i);
+ if (c instanceof NotPredicate) {
+ Predicate<ChangeData> n = c.getChild(0);
+ if (n instanceof TimestampRangePredicate) {
+ b.add(notTimestamp((TimestampRangePredicate<ChangeData>) n), MUST);
+ } else {
+ not.add(toQuery(n));
+ }
+ } else {
+ b.add(toQuery(c), MUST);
+ }
+ }
+ for (Query q : not) {
+ b.add(q, MUST_NOT);
+ }
+ return b;
+ } catch (BooleanQuery.TooManyClauses e) {
+ throw new QueryParseException("cannot create query for index: " + p, e);
+ }
+ }
+
+ private static Query not(Predicate<ChangeData> p) throws QueryParseException {
+ Predicate<ChangeData> n = p.getChild(0);
+ if (n instanceof TimestampRangePredicate) {
+ return notTimestamp((TimestampRangePredicate<ChangeData>) n);
+ }
+
+ // Lucene does not support negation, start with all and subtract.
+ BooleanQuery q = new BooleanQuery();
+ q.add(new MatchAllDocsQuery(), MUST);
+ q.add(toQuery(n), MUST_NOT);
return q;
}
@@ -121,8 +161,8 @@
private static Query sortKeyQuery(SortKeyPredicate p) {
return NumericRangeQuery.newLongRange(
p.getField().getName(),
- p.getMinValue(),
- p.getMaxValue(),
+ p.getMinValue() != Long.MIN_VALUE ? p.getMinValue() : null,
+ p.getMaxValue() != Long.MAX_VALUE ? p.getMaxValue() : null,
true, true);
}
@@ -140,7 +180,7 @@
throw new QueryParseException("not a timestamp: " + p);
}
- private static Query notTimestampQuery(TimestampRangePredicate<ChangeData> r)
+ private static Query notTimestamp(TimestampRangePredicate<ChangeData> r)
throws QueryParseException {
if (r.getMinTimestamp().getTime() == 0) {
return NumericRangeQuery.newIntRange(
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
index 3c5b0ab..f740fba 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -16,17 +16,8 @@
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
-import com.google.common.base.Stopwatch;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
-import com.google.common.util.concurrent.AsyncFunction;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningScheduledExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.lifecycle.LifecycleManager;
@@ -38,18 +29,14 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.cache.CacheRemovalListener;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MultiProgressMonitor;
-import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.index.ChangeBatchIndexer;
import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.index.IndexModule;
import com.google.gerrit.server.index.IndexModule.IndexType;
import com.google.gerrit.server.index.NoIndexModule;
import com.google.gerrit.server.patch.PatchListCacheImpl;
-import com.google.gerrit.server.patch.PatchListLoader;
-import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.solr.SolrIndexModule;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -61,51 +48,36 @@
import com.google.inject.ProvisionException;
import com.google.inject.TypeLiteral;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.eclipse.jgit.util.io.NullOutputStream;
import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import java.io.IOException;
import java.util.Collections;
-import java.util.Iterator;
import java.util.List;
-import java.util.Map;
import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
public class Reindex extends SiteProgram {
- private static final Logger log = LoggerFactory.getLogger(Reindex.class);
-
@Option(name = "--threads", usage = "Number of threads to use for indexing")
private int threads = Runtime.getRuntime().availableProcessors();
- @Option(name = "--dry-run", usage = "Dry run: don't write anything to index")
- private boolean dryRun;
+ @Option(name = "--schema-version",
+ usage = "Schema version to reindex; default is most recent version")
+ private Integer version;
+
+ @Option(name = "--output", usage = "Prefix for output; path for local disk index, or prefix for remote index")
+ private String outputBase;
@Option(name = "--verbose", usage = "Output debug information for each change")
private boolean verbose;
+ @Option(name = "--dry-run", usage = "Dry run: don't write anything to index")
+ private boolean dryRun;
+
private Injector dbInjector;
private Injector sysInjector;
+ private ChangeIndex index;
@Override
public int run() throws Exception {
@@ -114,22 +86,23 @@
if (IndexModule.getIndexType(dbInjector) == IndexType.SQL) {
throw die("index.type must be configured (or not SQL)");
}
-
+ if (version == null) {
+ version = ChangeSchemas.getLatest().getVersion();
+ }
LifecycleManager dbManager = new LifecycleManager();
dbManager.add(dbInjector);
dbManager.start();
sysInjector = createSysInjector();
-
- // Delete before any index may be created depending on this data.
- deleteAll();
-
LifecycleManager sysManager = new LifecycleManager();
sysManager.add(sysInjector);
sysManager.start();
+ index = sysInjector.getInstance(IndexCollection.class).getSearchIndex();
+ index.markReady(false);
+ index.deleteAll();
int result = indexAll();
- writeVersion();
+ index.markReady(true);
sysManager.stop();
dbManager.stop();
@@ -142,10 +115,10 @@
AbstractModule changeIndexModule;
switch (IndexModule.getIndexType(dbInjector)) {
case LUCENE:
- changeIndexModule = new LuceneIndexModule(false, threads, dryRun);
+ changeIndexModule = new LuceneIndexModule(version, threads, outputBase);
break;
case SOLR:
- changeIndexModule = new SolrIndexModule(false, threads);
+ changeIndexModule = new SolrIndexModule(false, threads, outputBase);
break;
default:
changeIndexModule = new NoIndexModule();
@@ -207,19 +180,8 @@
}
}
- private void deleteAll() throws IOException {
- if (dryRun) {
- return;
- }
- ChangeIndex index = sysInjector.getInstance(ChangeIndex.class);
- index.deleteAll();
- }
-
private int indexAll() throws Exception {
ReviewDb db = sysInjector.getInstance(ReviewDb.class);
- ListeningScheduledExecutorService executor = sysInjector.getInstance(
- Key.get(ListeningScheduledExecutorService.class, IndexExecutor.class));
-
ProgressMonitor pm = new TextProgressMonitor();
pm.start(1);
pm.beginTask("Collecting projects", ProgressMonitor.UNKNOWN);
@@ -237,243 +199,14 @@
}
pm.endTask();
- final MultiProgressMonitor mpm =
- new MultiProgressMonitor(System.err, "Reindexing changes");
- final Task projTask = mpm.beginSubTask("projects", projects.size());
- final Task doneTask = mpm.beginSubTask(null, changeCount);
- final Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
-
- Stopwatch sw = new Stopwatch().start();
- final List<ListenableFuture<?>> futures =
- Lists.newArrayListWithCapacity(projects.size());
- final AtomicBoolean ok = new AtomicBoolean(true);
-
- for (final Project.NameKey project : projects) {
- final ListenableFuture<?> future = executor.submit(
- new ReindexProject(project, doneTask, failedTask));
- futures.add(future);
- future.addListener(new Runnable() {
- @Override
- public void run() {
- try {
- future.get();
- } catch (InterruptedException e) {
- fail(project, e);
- } catch (ExecutionException e) {
- ok.set(false); // Logged by indexer.
- } catch (RuntimeException e) {
- failAndThrow(project, e);
- } catch (Error e) {
- failAndThrow(project, e);
- } finally {
- projTask.update(1);
- }
- }
-
- private void fail(Project.NameKey project, Throwable t) {
- log.error("Failed to index project " + project, t);
- ok.set(false);
- }
-
- private void failAndThrow(Project.NameKey project, RuntimeException e) {
- fail(project, e);
- throw e;
- }
-
- private void failAndThrow(Project.NameKey project, Error e) {
- fail(project, e);
- throw e;
- }
- }, MoreExecutors.sameThreadExecutor());
- }
-
- mpm.waitFor(Futures.transform(Futures.successfulAsList(futures),
- new AsyncFunction<List<?>, Void>() {
- @Override
- public ListenableFuture<Void> apply(List<?> input) throws Exception {
- mpm.end();
- return Futures.immediateFuture(null);
- }
- }));
- double elapsed = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
- int n = doneTask.getCount() + failedTask.getCount();
- System.out.format("Reindexed %d changes in %.01fs (%.01f/s)\n",
- n, elapsed, n/elapsed);
-
- return ok.get() ? 0 : 1;
- }
-
- private class ReindexProject implements Callable<Void> {
- private final ChangeIndexer indexer;
- private final Project.NameKey project;
- private final ListMultimap<ObjectId, ChangeData> byId;
- private final Task done;
- private final Task failed;
- private Repository repo;
- private RevWalk walk;
-
- private ReindexProject(Project.NameKey project, Task done, Task failed) {
- this.indexer = sysInjector.getInstance(ChangeIndexer.class);
- this.project = project;
- this.byId = ArrayListMultimap.create();
- this.done = done;
- this.failed = failed;
- }
-
- @Override
- public Void call() throws Exception {
- ReviewDb db = sysInjector.getInstance(ReviewDb.class);
- GitRepositoryManager mgr = sysInjector.getInstance(GitRepositoryManager.class);
- repo = mgr.openRepository(project);
-
- try {
- Map<String, Ref> refs = repo.getAllRefs();
- for (Change c : db.changes().byProject(project)) {
- String refName = c.currentPatchSetId().toRefName();
- Ref r = refs.get(refName);
- if (r != null) {
- byId.put(r.getObjectId(), new ChangeData(c));
- } else {
- fail("Failed to index change " + c.getId()
- + " (" + refName + " not found)", true, null);
- }
- }
- walk();
- } finally {
- repo.close();
- RepositoryCache.close(repo); // Only used once per Reindex call.
- }
- return null;
- }
-
- private void walk() throws Exception {
- walk = new RevWalk(repo);
- try {
- // Walk only refs first to cover as many changes as we can without having
- // to mark every single change.
- for (Ref ref : repo.getRefDatabase().getRefs(Constants.R_HEADS).values()) {
- RevObject o = walk.parseAny(ref.getObjectId());
- if (o instanceof RevCommit) {
- walk.markStart((RevCommit) o);
- }
- }
-
- RevCommit bCommit;
- while ((bCommit = walk.next()) != null && !byId.isEmpty()) {
- if (byId.containsKey(bCommit)) {
- getPathsAndIndex(bCommit);
- byId.removeAll(bCommit);
- }
- }
-
- for (ObjectId id : byId.keySet()) {
- getPathsAndIndex(walk.parseCommit(id));
- }
- } finally {
- walk.release();
- }
- }
-
- private void getPathsAndIndex(RevCommit bCommit) throws Exception {
- RevTree bTree = bCommit.getTree();
- List<ChangeData> cds = Lists.newArrayList(byId.get(bCommit));
- try {
- RevTree aTree = aFor(bCommit, walk);
- DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
- try {
- df.setRepository(repo);
- if (!cds.isEmpty()) {
- List<String> paths = (aTree != null)
- ? getPaths(df.scan(aTree, bTree))
- : Collections.<String>emptyList();
- Iterator<ChangeData> cdit = cds.iterator();
- for (ChangeData cd ; cdit.hasNext(); cdit.remove()) {
- cd = cdit.next();
- try {
- cd.setCurrentFilePaths(paths);
- indexer.indexTask(cd).call();
- done.update(1);
- if (verbose) {
- System.out.println("Reindexed change " + cd.getId());
- }
- } catch (Exception e) {
- fail("Failed to index change " + cd.getId(), true, e);
- }
- }
- }
- } finally {
- df.release();
- }
- } catch (Exception e) {
- fail("Failed to index commit " + bCommit.name(), false, e);
- for (ChangeData cd : cds) {
- fail("Failed to index change " + cd.getId(), true, null);
- }
- }
- }
-
- private List<String> getPaths(List<DiffEntry> filenames) {
- Set<String> paths = Sets.newTreeSet();
- for (DiffEntry e : filenames) {
- if (e.getOldPath() != null) {
- paths.add(e.getOldPath());
- }
- if (e.getNewPath() != null) {
- paths.add(e.getNewPath());
- }
- }
- return ImmutableList.copyOf(paths);
- }
-
- private RevTree aFor(RevCommit b, RevWalk walk) throws IOException {
- switch (b.getParentCount()) {
- case 0:
- return walk.parseTree(emptyTree());
- case 1:
- RevCommit a = b.getParent(0);
- walk.parseBody(a);
- return walk.parseTree(a.getTree());
- case 2:
- return PatchListLoader.automerge(repo, walk, b);
- default:
- return null;
- }
- }
-
- private ObjectId emptyTree() throws IOException {
- ObjectInserter oi = repo.newObjectInserter();
- try {
- ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
- oi.flush();
- return id;
- } finally {
- oi.release();
- }
- }
-
- private void fail(String error, boolean failed, Exception e) {
- if (failed) {
- this.failed.update(1);
- }
-
- if (e != null) {
- log.warn(error, e);
- } else {
- log.warn(error);
- }
-
- if (verbose) {
- System.out.println(error);
- }
- }
- }
-
- private void writeVersion() throws IOException,
- ConfigInvalidException {
- if (dryRun) {
- return;
- }
- ChangeIndex index = sysInjector.getInstance(ChangeIndex.class);
- index.finishIndex();
+ ChangeBatchIndexer batchIndexer =
+ sysInjector.getInstance(ChangeBatchIndexer.class);
+ ChangeBatchIndexer.Result result = batchIndexer.indexAll(
+ index, projects, projects.size(), changeCount, System.err,
+ verbose ? System.out : NullOutputStream.INSTANCE);
+ int n = result.doneCount() + result.failedCount();
+ double t = result.elapsed(TimeUnit.MILLISECONDS) / 1000d;
+ System.out.format("Reindexed %d changes in %.01fs (%.01f/s)\n", n, t, n/t);
+ return result.success() ? 0 : 1;
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index bf0af7f..415c55a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -91,6 +91,7 @@
extractMailExample("Comment.vm");
extractMailExample("CommentFooter.vm");
extractMailExample("CommitMessageEdited.vm");
+ extractMailExample("Footer.vm");
extractMailExample("Merged.vm");
extractMailExample("MergeFail.vm");
extractMailExample("NewChange.vm");
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
index 9f76a47..8e7c699 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
@@ -60,13 +60,12 @@
@Override
protected String prettify(String html, String type) {
- return go(prettify.getContext(), html, type, diffPrefs.getTabSize());
+ return go(prettify.getContext(), html, type);
}
private static native String go(JavaScriptObject ctx, String srcText,
- String srcType, int tabSize)
+ String srcType)
/*-{
- ctx.PR_TAB_WIDTH = tabSize;
return ctx.prettyPrintOne(srcText, srcType);
}-*/;
}
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
index 2836f33..a84af5e 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -129,6 +129,7 @@
String html = toHTML(src);
+ html = expandTabs(html);
if (diffPrefs.isSyntaxHighlighting() && getFileType() != null
&& src.isWholeFile()) {
// The prettify parsers don't like ' as an entity for the
@@ -146,11 +147,9 @@
// Drop any '\r' to avoid this problem.
html = html.replace("\r</span>\n", "</span>\n");
- html = html.replace("\n", " \n");
+ html = html.replaceAll("(\r)?\n", " $1\n");
html = prettify(html, getFileType());
- html = html.replace(" \n", "\n");
- } else {
- html = expandTabs(html);
+ html = html.replaceAll(" (\r)?\n", "$1\n");
}
int pos = 0;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
index 7e0b90c..703edbb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
@@ -30,10 +30,10 @@
@Query("WHERE id.changeId = ? ORDER BY id.patchSetId")
ResultSet<PatchSet> byChange(Change.Id id) throws OrmException;
- @Query("WHERE revision = ? LIMIT 2")
+ @Query("WHERE revision = ?")
ResultSet<PatchSet> byRevision(RevId rev) throws OrmException;
- @Query("WHERE revision >= ? AND revision <= ? LIMIT 2")
+ @Query("WHERE revision >= ? AND revision <= ?")
ResultSet<PatchSet> byRevisionRange(RevId reva, RevId revb)
throws OrmException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
index 1c66555..a96e713 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
@@ -16,6 +16,7 @@
import com.google.common.base.Strings;
import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
@@ -24,6 +25,8 @@
import org.kohsuke.args4j.Option;
+import java.util.concurrent.TimeUnit;
+
class GetAvatar implements RestReadView<AccountResource> {
private final DynamicItem<AvatarProvider> avatarProvider;
@@ -41,12 +44,14 @@
throws ResourceNotFoundException {
AvatarProvider impl = avatarProvider.get();
if (impl == null) {
- throw new ResourceNotFoundException();
+ throw (new ResourceNotFoundException())
+ .caching(CacheControl.PUBLIC(1, TimeUnit.DAYS));
}
String url = impl.getUrl(rsrc.getUser(), size);
if (Strings.isNullOrEmpty(url)) {
- throw new ResourceNotFoundException();
+ throw (new ResourceNotFoundException())
+ .caching(CacheControl.PUBLIC(1, TimeUnit.HOURS));
} else {
return Response.redirect(url);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 2a8e7c9..2e01f26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -127,10 +127,15 @@
/** Can this user see this group exists? */
public boolean isVisible() {
AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
+ /* Check for canAdministrateServer may seem redundant, but allows
+ * for visibility of all groups that are not an internal group to
+ * server administrators.
+ */
return (accountGroup != null && accountGroup.isVisibleToAll())
|| user instanceof InternalUser
|| user.getEffectiveGroups().contains(group.getGroupUUID())
- || isOwner();
+ || isOwner()
+ || user.getCapabilities().canAdministrateServer();
}
public boolean isOwner() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 54f4f29..89ec948 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -16,9 +16,11 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -29,6 +31,8 @@
import org.kohsuke.args4j.Option;
+import java.util.concurrent.TimeUnit;
+
class Files implements ChildCollection<RevisionResource, FileResource> {
private final DynamicMap<RestView<FileResource>> views;
private final FileInfoJson fileInfoJson;
@@ -73,8 +77,11 @@
resource.getChangeResource(), IdString.fromDecoded(base));
basePatchSet = baseResource.getPatchSet();
}
- return fileInfoJson.toFileInfoMap(
- resource.getChange(), resource.getPatchSet(), basePatchSet);
+ return Response.ok(fileInfoJson.toFileInfoMap(
+ resource.getChange(),
+ resource.getPatchSet(),
+ basePatchSet))
+ .caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
index b3cd813..3d9c0fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
@@ -19,9 +19,21 @@
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import org.kohsuke.args4j.Option;
+
public class GetDetail implements RestReadView<ChangeResource> {
private final ChangeJson json;
+ @Option(name = "-o", multiValued = true, usage = "Output options")
+ void addOption(ListChangesOption o) {
+ json.addOption(o);
+ }
+
+ @Option(name = "-O", usage = "Output option flags, in hex")
+ void setOptionFlagsHex(String hex) {
+ json.addOptions(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+ }
+
@Inject
GetDetail(ChangeJson json) {
this.json = json
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 679d32b..f01c961 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -22,6 +22,7 @@
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.common.data.PatchScript.FileMode;
+import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
@@ -49,6 +50,7 @@
import org.kohsuke.args4j.spi.Setter;
import java.util.List;
+import java.util.concurrent.TimeUnit;
public class GetDiff implements RestReadView<FileResource> {
private final PatchScriptFactory.Factory patchScriptFactoryFactory;
@@ -150,7 +152,7 @@
result.diffHeader = ps.getPatchHeader();
}
result.content = content.lines;
- return Response.ok(result).caching(Response.CacheControl.PRIVATE);
+ return Response.ok(result).caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
}
static class Result {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 8317859..9f410b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -16,10 +16,14 @@
import static com.google.common.base.Preconditions.checkArgument;
+import com.google.common.collect.Sets;
import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
@@ -31,6 +35,7 @@
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.RefControl;
@@ -48,13 +53,19 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
public class PatchSetInserter {
+ private static final Logger log =
+ LoggerFactory.getLogger(PatchSetInserter.class);
+
public static interface Factory {
PatchSetInserter create(Repository git, RevWalk revWalk, RefControl refControl,
Change change, RevCommit commit);
@@ -69,6 +80,7 @@
private final CommitValidators.Factory commitValidatorsFactory;
private final ChangeIndexer indexer;
private boolean validateForReceiveCommits;
+ private final ReplacePatchSetSender.Factory replacePatchSetFactory;
private final Repository git;
private final RevWalk revWalk;
@@ -90,6 +102,7 @@
GitReferenceUpdated gitRefUpdated,
CommitValidators.Factory commitValidatorsFactory,
ChangeIndexer indexer,
+ ReplacePatchSetSender.Factory replacePatchSetFactory,
@Assisted Repository git,
@Assisted RevWalk revWalk,
@Assisted RefControl refControl,
@@ -103,6 +116,7 @@
this.gitRefUpdated = gitRefUpdated;
this.commitValidatorsFactory = commitValidatorsFactory;
this.indexer = indexer;
+ this.replacePatchSetFactory = replacePatchSetFactory;
this.git = git;
this.revWalk = revWalk;
@@ -178,6 +192,18 @@
ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
db.patchSets().insert(Collections.singleton(patchSet));
+ final List<PatchSetApproval> oldPatchSetApprovals =
+ db.patchSetApprovals().byChange(change.getId()).toList();
+ final Set<Account.Id> oldReviewers = Sets.newHashSet();
+ final Set<Account.Id> oldCC = Sets.newHashSet();
+ for (PatchSetApproval a : oldPatchSetApprovals) {
+ if (a.getValue() != 0) {
+ oldReviewers.add(a.getAccountId());
+ } else {
+ oldCC.add(a.getAccountId());
+ }
+ }
+
updatedChange =
db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
@Override
@@ -216,6 +242,21 @@
db.changeMessages().insert(Collections.singleton(changeMessage));
}
+ try {
+ PatchSetInfo info = patchSetInfoFactory.get(commit, patchSet.getId());
+ ReplacePatchSetSender cm =
+ replacePatchSetFactory.create(updatedChange);
+ cm.setFrom(user.getAccountId());
+ cm.setPatchSet(patchSet, info);
+ cm.setChangeMessage(changeMessage);
+ cm.addReviewers(oldReviewers);
+ cm.addExtraCC(oldCC);
+ cm.send();
+ } catch (Exception err) {
+ log.error("Cannot send email for new patch set on change " + updatedChange.getId(),
+ err);
+ }
+
indexer.index(updatedChange);
hooks.doPatchsetCreatedHook(updatedChange, patchSet, db);
} finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 7b242d9..2e69ff0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -106,7 +106,6 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.gerrit.server.ssh.SshAddressesModule;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.util.IdGenerator;
@@ -172,7 +171,6 @@
install(ThreadLocalRequestContext.module());
bind(AccountResolver.class);
- bind(ChangeQueryRewriter.class);
factory(AccountInfoCacheFactory.Factory.class);
factory(AddReviewerSender.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java
new file mode 100644
index 0000000..f618959c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2013 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.config;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+public class GetVersion implements RestReadView<ConfigResource> {
+ @Override
+ public String apply(ConfigResource resource) throws ResourceNotFoundException {
+ String version = Version.getVersion();
+ if (version == null) {
+ throw new ResourceNotFoundException();
+ }
+ return version;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index 81c2de9..0ea7390 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -26,5 +26,6 @@
DynamicMap.mapOf(binder(), CONFIG_KIND);
DynamicMap.mapOf(binder(), CAPABILITY_KIND);
child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
+ get(CONFIG_KIND, "version").to(GetVersion.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
index 79d82e3..91df974 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
@@ -24,6 +24,7 @@
public AccountAttribute uploader;
public Long createdOn;
public AccountAttribute author;
+ public boolean isDraft;
public List<ApprovalAttribute> approvals;
public List<PatchSetCommentAttribute> comments;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index bbef3ad..084b79b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -361,6 +361,7 @@
p.ref = patchSet.getRefName();
p.uploader = asAccountAttribute(patchSet.getUploader());
p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
+ p.isDraft = patchSet.isDraft();
final PatchSet.Id pId = patchSet.getId();
try {
final ReviewDb db = schema.open();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 5a18023..8bd8c0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -729,7 +729,7 @@
set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), Project.InheritableBoolean.INHERIT);
- set(rc, PROJECT, null, KEY_STATE, p.getState(), null);
+ set(rc, PROJECT, null, KEY_STATE, p.getState(), defaultStateValue);
set(rc, DASHBOARD, null, KEY_DEFAULT, p.getDefaultDashboard());
set(rc, DASHBOARD, null, KEY_LOCAL_DEFAULT, p.getLocalDefaultDashboard());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 487b177..2c8663e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -17,7 +17,6 @@
import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
-
import static org.eclipse.jgit.lib.Constants.R_HEADS;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
@@ -62,6 +61,7 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeResource;
@@ -262,6 +262,7 @@
private final CommitValidators.Factory commitValidatorsFactory;
private final TrackingFooters trackingFooters;
private final TagCache tagCache;
+ private final AccountCache accountCache;
private final ChangeInserter.Factory changeInserterFactory;
private final WorkQueue workQueue;
private final ListeningExecutorService changeUpdateExector;
@@ -318,6 +319,7 @@
final ProjectCache projectCache,
final GitRepositoryManager repoManager,
final TagCache tagCache,
+ final AccountCache accountCache,
final ChangeCache changeCache,
final ChangeInserter.Factory changeInserterFactory,
final CommitValidators.Factory commitValidatorsFactory,
@@ -353,6 +355,7 @@
this.canonicalWebUrl = canonicalWebUrl;
this.trackingFooters = trackingFooters;
this.tagCache = tagCache;
+ this.accountCache = accountCache;
this.changeInserterFactory = changeInserterFactory;
this.commitValidatorsFactory = commitValidatorsFactory;
this.workQueue = workQueue;
@@ -2055,6 +2058,7 @@
return;
}
+ boolean defaultName = Strings.isNullOrEmpty(currentUser.getAccount().getFullName());
final RevWalk walk = rp.getRevWalk();
walk.reset();
walk.sort(RevSort.NONE);
@@ -2070,6 +2074,23 @@
} else if (!validCommit(ctl, cmd, c)) {
break;
}
+
+ if (defaultName && currentUser.getEmailAddresses().contains(
+ c.getCommitterIdent().getEmailAddress())) {
+ try {
+ Account a = db.accounts().get(currentUser.getAccountId());
+ if (a != null && Strings.isNullOrEmpty(a.getFullName())) {
+ a.setFullName(c.getCommitterIdent().getName());
+ db.accounts().update(Collections.singleton(a));
+ currentUser.getAccount().setFullName(a.getFullName());
+ accountCache.evict(a.getId());
+ }
+ } catch (OrmException e) {
+ log.warn("Cannot default full_name", e);
+ } finally {
+ defaultName = false;
+ }
+ }
}
} catch (IOException err) {
cmd.setResult(REJECTED_MISSING_OBJECT);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
new file mode 100644
index 0000000..261a439
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
@@ -0,0 +1,362 @@
+// Copyright (C) 2013 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.git;
+
+package com.google.gerrit.server.index;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+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.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.patch.PatchListLoader;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.eclipse.jgit.util.io.NullOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ChangeBatchIndexer {
+ private static final Logger log =
+ LoggerFactory.getLogger(ChangeBatchIndexer.class);
+
+ public static class Result {
+ private final long elapsedNanos;
+ private final boolean success;
+ private final int done;
+ private final int failed;
+
+ private Result(Stopwatch sw, boolean success, int done, int failed) {
+ this.elapsedNanos = sw.elapsed(TimeUnit.NANOSECONDS);
+ this.success = success;
+ this.done = done;
+ this.failed = failed;
+ }
+
+ public boolean success() {
+ return success;
+ }
+
+ public int doneCount() {
+ return done;
+ }
+
+ public int failedCount() {
+ return failed;
+ }
+
+ public long elapsed(TimeUnit timeUnit) {
+ return timeUnit.convert(elapsedNanos, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ private final SchemaFactory<ReviewDb> schemaFactory;
+ private final GitRepositoryManager repoManager;
+ private final ListeningScheduledExecutorService executor;
+ private final ChangeIndexer.Factory indexerFactory;
+
+ @Inject
+ ChangeBatchIndexer(SchemaFactory<ReviewDb> schemaFactory,
+ GitRepositoryManager repoManager,
+ @IndexExecutor ListeningScheduledExecutorService executor,
+ ChangeIndexer.Factory indexerFactory) {
+ this.schemaFactory = schemaFactory;
+ this.repoManager = repoManager;
+ this.executor = executor;
+ this.indexerFactory = indexerFactory;
+ }
+
+ public Result indexAll(ChangeIndex index, Iterable<Project.NameKey> projects,
+ int numProjects, int numChanges, OutputStream progressOut,
+ OutputStream verboseOut) {
+ if (progressOut == null) {
+ progressOut = NullOutputStream.INSTANCE;
+ }
+ PrintWriter verboseWriter = verboseOut != null ? new PrintWriter(verboseOut)
+ : null;
+
+ Stopwatch sw = new Stopwatch().start();
+ final MultiProgressMonitor mpm =
+ new MultiProgressMonitor(progressOut, "Reindexing changes");
+ final Task projTask = mpm.beginSubTask("projects",
+ numProjects >= 0 ? numProjects : MultiProgressMonitor.UNKNOWN);
+ final Task doneTask = mpm.beginSubTask(null,
+ numChanges >= 0 ? numChanges : MultiProgressMonitor.UNKNOWN);
+ final Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
+
+ final List<ListenableFuture<?>> futures = Lists.newArrayList();
+ final AtomicBoolean ok = new AtomicBoolean(true);
+
+ for (final Project.NameKey project : projects) {
+ final ListenableFuture<?> future = executor.submit(new ReindexProject(
+ indexerFactory.create(index), project, doneTask, failedTask,
+ verboseWriter));
+ futures.add(future);
+ future.addListener(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ future.get();
+ } catch (InterruptedException e) {
+ fail(project, e);
+ } catch (ExecutionException e) {
+ fail(project, e);
+ } catch (RuntimeException e) {
+ failAndThrow(project, e);
+ } catch (Error e) {
+ failAndThrow(project, e);
+ } finally {
+ projTask.update(1);
+ }
+ }
+
+ private void fail(Project.NameKey project, Throwable t) {
+ log.error("Failed to index project " + project, t);
+ ok.set(false);
+ }
+
+ private void failAndThrow(Project.NameKey project, RuntimeException e) {
+ fail(project, e);
+ throw e;
+ }
+
+ private void failAndThrow(Project.NameKey project, Error e) {
+ fail(project, e);
+ throw e;
+ }
+ }, MoreExecutors.sameThreadExecutor());
+ }
+
+ try {
+ mpm.waitFor(Futures.transform(Futures.successfulAsList(futures),
+ new AsyncFunction<List<?>, Void>() {
+ @Override
+ public ListenableFuture<Void> apply(List<?> input) {
+ mpm.end();
+ return Futures.immediateFuture(null);
+ }
+ }));
+ } catch (ExecutionException e) {
+ log.error("Error in batch indexer", e);
+ ok.set(false);
+ }
+ return new Result(sw, ok.get(), doneTask.getCount(), failedTask.getCount());
+ }
+
+ private class ReindexProject implements Callable<Void> {
+ private final ChangeIndexer indexer;
+ private final Project.NameKey project;
+ private final ListMultimap<ObjectId, ChangeData> byId;
+ private final Task done;
+ private final Task failed;
+ private final PrintWriter verboseWriter;
+ private Repository repo;
+ private RevWalk walk;
+
+ private ReindexProject(ChangeIndexer indexer, Project.NameKey project,
+ Task done, Task failed, PrintWriter verboseWriter) {
+ this.indexer = indexer;
+ this.project = project;
+ this.byId = ArrayListMultimap.create();
+ this.done = done;
+ this.verboseWriter = verboseWriter;
+ this.failed = failed;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ ReviewDb db = schemaFactory.open();
+ try {
+ repo = repoManager.openRepository(project);
+ try {
+ Map<String, Ref> refs = repo.getAllRefs();
+ for (Change c : db.changes().byProject(project)) {
+ Ref r = refs.get(c.currentPatchSetId().toRefName());
+ if (r != null) {
+ byId.put(r.getObjectId(), new ChangeData(c));
+ }
+ }
+ walk();
+ } finally {
+ repo.close();
+ // TODO(dborowitz): Opening all repositories in a live server may be
+ // wasteful; see if we can determine which ones it is safe to close with
+ // RepositoryCache.close(repo).
+ }
+ } finally {
+ db.close();
+ }
+ return null;
+ }
+
+ private void walk() throws Exception {
+ walk = new RevWalk(repo);
+ try {
+ // Walk only refs first to cover as many changes as we can without having
+ // to mark every single change.
+ for (Ref ref : repo.getRefDatabase().getRefs(Constants.R_HEADS).values()) {
+ RevObject o = walk.parseAny(ref.getObjectId());
+ if (o instanceof RevCommit) {
+ walk.markStart((RevCommit) o);
+ }
+ }
+
+ RevCommit bCommit;
+ while ((bCommit = walk.next()) != null && !byId.isEmpty()) {
+ if (byId.containsKey(bCommit)) {
+ getPathsAndIndex(bCommit);
+ byId.removeAll(bCommit);
+ }
+ }
+
+ for (ObjectId id : byId.keySet()) {
+ getPathsAndIndex(walk.parseCommit(id));
+ }
+ } finally {
+ walk.release();
+ }
+ }
+
+ private void getPathsAndIndex(RevCommit bCommit) throws Exception {
+ RevTree bTree = bCommit.getTree();
+ List<ChangeData> cds = Lists.newArrayList(byId.get(bCommit));
+ try {
+ RevTree aTree = aFor(bCommit, walk);
+ DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+ try {
+ df.setRepository(repo);
+ if (!cds.isEmpty()) {
+ List<String> paths = (aTree != null)
+ ? getPaths(df.scan(aTree, bTree))
+ : Collections.<String>emptyList();
+ Iterator<ChangeData> cdit = cds.iterator();
+ for (ChangeData cd ; cdit.hasNext(); cdit.remove()) {
+ cd = cdit.next();
+ try {
+ cd.setCurrentFilePaths(paths);
+ indexer.indexTask(cd).call();
+ done.update(1);
+ if (verboseWriter != null) {
+ verboseWriter.println("Reindexed change " + cd.getId());
+ }
+ } catch (Exception e) {
+ fail("Failed to index change " + cd.getId(), true, e);
+ }
+ }
+ }
+ } finally {
+ df.release();
+ }
+ } catch (Exception e) {
+ fail("Failed to index commit " + bCommit.name(), false, e);
+ for (ChangeData cd : cds) {
+ fail("Failed to index change " + cd.getId(), true, null);
+ }
+ }
+ }
+
+ private List<String> getPaths(List<DiffEntry> filenames) {
+ Set<String> paths = Sets.newTreeSet();
+ for (DiffEntry e : filenames) {
+ if (e.getOldPath() != null) {
+ paths.add(e.getOldPath());
+ }
+ if (e.getNewPath() != null) {
+ paths.add(e.getNewPath());
+ }
+ }
+ return ImmutableList.copyOf(paths);
+ }
+
+ private RevTree aFor(RevCommit b, RevWalk walk) throws IOException {
+ switch (b.getParentCount()) {
+ case 0:
+ return walk.parseTree(emptyTree());
+ case 1:
+ RevCommit a = b.getParent(0);
+ walk.parseBody(a);
+ return walk.parseTree(a.getTree());
+ case 2:
+ return PatchListLoader.automerge(repo, walk, b);
+ default:
+ return null;
+ }
+ }
+
+ private ObjectId emptyTree() throws IOException {
+ ObjectInserter oi = repo.newObjectInserter();
+ try {
+ ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
+ oi.flush();
+ return id;
+ } finally {
+ oi.release();
+ }
+ }
+
+ private void fail(String error, boolean failed, Exception e) {
+ if (failed) {
+ this.failed.update(1);
+ }
+
+ if (e != null) {
+ log.warn(error, e);
+ } else {
+ log.warn(error);
+ }
+
+ if (verboseWriter != null) {
+ verboseWriter.println(error);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index f214dce..d62df95 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -14,8 +14,6 @@
package com.google.gerrit.server.index;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -30,11 +28,7 @@
import com.google.gwtorm.server.OrmException;
import java.io.IOException;
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.ParameterizedType;
import java.sql.Timestamp;
-import java.util.Map;
import java.util.Set;
/**
@@ -44,13 +38,8 @@
* {@link ChangeQueryBuilder} for querying that field, and a method on
* {@link ChangeData} used for populating the corresponding document fields in
* the secondary index.
- * <p>
- * Used to generate a schema for index implementations that require one.
*/
public class ChangeField {
- /** Increment whenever making schema changes. */
- public static final int SCHEMA_VERSION = 15;
-
/** Legacy change ID. */
public static final FieldDef<ChangeData, Integer> LEGACY_ID =
new FieldDef.Single<ChangeData, Integer>("_id",
@@ -284,36 +273,4 @@
return r;
}
};
-
- public static final ImmutableMap<String, FieldDef<ChangeData, ?>> ALL;
-
- static {
- Map<String, FieldDef<ChangeData, ?>> fields = Maps.newHashMap();
- for (Field f : ChangeField.class.getFields()) {
- if (Modifier.isPublic(f.getModifiers())
- && Modifier.isStatic(f.getModifiers())
- && Modifier.isFinal(f.getModifiers())
- && FieldDef.class.isAssignableFrom(f.getType())) {
- ParameterizedType t = (ParameterizedType) f.getGenericType();
- if (t.getActualTypeArguments()[0] == ChangeData.class) {
- try {
- @SuppressWarnings("unchecked")
- FieldDef<ChangeData, ?> fd = (FieldDef<ChangeData, ?>) f.get(null);
- fields.put(fd.getName(), fd);
- } catch (IllegalArgumentException e) {
- throw new ExceptionInInitializerError(e);
- } catch (IllegalAccessException e) {
- throw new ExceptionInInitializerError(e);
- }
- } else {
- throw new ExceptionInInitializerError(
- "non-ChangeData ChangeField: " + f);
- }
- }
- }
- if (fields.isEmpty()) {
- throw new ExceptionInInitializerError("no ChangeFields found");
- }
- ALL = ImmutableMap.copyOf(fields);
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
index 3745eb5..e411956 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -21,8 +21,6 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeDataSource;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
import java.io.IOException;
/**
@@ -39,6 +37,11 @@
/** Instance indicating secondary index is disabled. */
public static final ChangeIndex DISABLED = new ChangeIndex() {
@Override
+ public Schema<ChangeData> getSchema() {
+ return null;
+ }
+
+ @Override
public ListenableFuture<Void> insert(ChangeData cd) throws IOException {
return Futures.immediateFuture(null);
}
@@ -59,17 +62,27 @@
}
@Override
- public ChangeDataSource getSource(Predicate<ChangeData> p)
- throws QueryParseException {
+ public ChangeDataSource getSource(Predicate<ChangeData> p) {
throw new UnsupportedOperationException();
}
@Override
- public void finishIndex() {
+ public void close() {
// Do nothing.
}
+
+ @Override
+ public void markReady(boolean ready) {
+ throw new UnsupportedOperationException();
+ }
};
+ /** @return the schema version used by this index. */
+ public Schema<ChangeData> getSchema();
+
+ /** Close this index. */
+ public void close();
+
/**
* Insert a change document into the index.
* <p>
@@ -129,11 +142,10 @@
throws QueryParseException;
/**
- * Mark completion of indexing.
+ * Mark whether this index is up-to-date and ready to serve reads.
*
- * @throws ConfigInvalidException
+ * @param ready whether the index is ready
* @throws IOException
*/
- public void finishIndex() throws IOException,
- ConfigInvalidException;
+ public void markReady(boolean ready) throws IOException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
index c25174e..0aa3fc8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -29,6 +29,11 @@
* compute some of the fields and/or update the index.
*/
public abstract class ChangeIndexer {
+ public interface Factory {
+ ChangeIndexer create(ChangeIndex index);
+ ChangeIndexer create(IndexCollection indexes);
+ }
+
/** Instance indicating secondary index is disabled. */
public static final ChangeIndexer DISABLED = new ChangeIndexer(null) {
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
index 2c060ed..1de5c99 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
@@ -21,9 +21,10 @@
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import com.google.inject.util.Providers;
import org.slf4j.Logger;
@@ -41,19 +42,33 @@
private static final Logger log =
LoggerFactory.getLogger(ChangeIndexerImpl.class);
+ private final IndexCollection indexes;
private final ChangeIndex index;
private final SchemaFactory<ReviewDb> schemaFactory;
private final ThreadLocalRequestContext context;
- @Inject
+ @AssistedInject
ChangeIndexerImpl(@IndexExecutor ListeningScheduledExecutorService executor,
- ChangeIndex index,
SchemaFactory<ReviewDb> schemaFactory,
- ThreadLocalRequestContext context) {
+ ThreadLocalRequestContext context,
+ @Assisted ChangeIndex index) {
super(executor);
- this.index = index;
this.schemaFactory = schemaFactory;
this.context = context;
+ this.index = index;
+ this.indexes = null;
+ }
+
+ @AssistedInject
+ ChangeIndexerImpl(@IndexExecutor ListeningScheduledExecutorService executor,
+ SchemaFactory<ReviewDb> schemaFactory,
+ ThreadLocalRequestContext context,
+ @Assisted IndexCollection indexes) {
+ super(executor);
+ this.schemaFactory = schemaFactory;
+ this.context = context;
+ this.index = null;
+ this.indexes = indexes;
}
@Override
@@ -84,7 +99,13 @@
throw new OutOfScopeException("No user during ChangeIndexer");
}
});
- index.replace(cd);
+ if (indexes != null) {
+ for (ChangeIndex i : indexes.getWriteIndexes()) {
+ i.replace(cd); // TODO(dborowitz): Parallelize these
+ }
+ } else {
+ index.replace(cd);
+ }
return null;
} finally {
context.setContext(null);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
new file mode 100644
index 0000000..87a4df1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2013 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.git;
+
+package com.google.gerrit.server.index;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.query.change.ChangeData;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.util.Arrays;
+import java.util.Map;
+
+/** Secondary index schemas for changes. */
+public class ChangeSchemas {
+ @SuppressWarnings("unchecked")
+ static final Schema<ChangeData> V1 = release(
+ ChangeField.LEGACY_ID,
+ ChangeField.ID,
+ ChangeField.STATUS,
+ ChangeField.PROJECT,
+ ChangeField.REF,
+ ChangeField.TOPIC,
+ ChangeField.UPDATED,
+ ChangeField.SORTKEY,
+ ChangeField.FILE,
+ ChangeField.OWNER,
+ ChangeField.REVIEWER,
+ ChangeField.COMMIT,
+ ChangeField.TR,
+ ChangeField.LABEL,
+ ChangeField.REVIEWED,
+ ChangeField.COMMIT_MESSAGE,
+ ChangeField.COMMENT);
+
+ private static Schema<ChangeData> release(FieldDef<ChangeData, ?>... fields) {
+ return new Schema<ChangeData>(true, Arrays.asList(fields));
+ }
+
+ @SuppressWarnings("unused")
+ private static Schema<ChangeData> developer(FieldDef<ChangeData, ?>... fields) {
+ return new Schema<ChangeData>(false, Arrays.asList(fields));
+ }
+
+ public static final ImmutableMap<Integer, Schema<ChangeData>> ALL;
+
+ public static Schema<ChangeData> get(int version) {
+ Schema<ChangeData> schema = ALL.get(version);
+ checkArgument(schema != null, "Unrecognized schema version: %s", version);
+ return schema;
+ }
+
+ public static Schema<ChangeData> getLatest() {
+ return Iterables.getLast(ALL.values());
+ }
+
+ static {
+ Map<Integer, Schema<ChangeData>> all = Maps.newTreeMap();
+ for (Field f : ChangeSchemas.class.getDeclaredFields()) {
+ if (Modifier.isStatic(f.getModifiers())
+ && Modifier.isFinal(f.getModifiers())
+ && Schema.class.isAssignableFrom(f.getType())) {
+ ParameterizedType t = (ParameterizedType) f.getGenericType();
+ if (t.getActualTypeArguments()[0] == ChangeData.class) {
+ try {
+ @SuppressWarnings("unchecked")
+ Schema<ChangeData> schema = (Schema<ChangeData>) f.get(null);
+ checkArgument(f.getName().startsWith("V"));
+ schema.setVersion(Integer.parseInt(f.getName().substring(1)));
+ all.put(schema.getVersion(), schema);
+ } catch (IllegalArgumentException e) {
+ throw new ExceptionInInitializerError(e);
+ } catch (IllegalAccessException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ } else {
+ throw new ExceptionInInitializerError(
+ "non-ChangeData schema: " + f);
+ }
+ }
+ }
+ if (all.isEmpty()) {
+ throw new ExceptionInInitializerError("no ChangeSchemas found");
+ }
+ ALL = ImmutableMap.copyOf(all);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
new file mode 100644
index 0000000..09561208
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 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.git;
+
+package com.google.gerrit.server.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/** Dynamic pointers to the index versions used for searching and writing. */
+@Singleton
+public class IndexCollection implements LifecycleListener {
+ private final CopyOnWriteArrayList<ChangeIndex> writeIndexes;
+ private final AtomicReference<ChangeIndex> searchIndex;
+
+ @Inject
+ @VisibleForTesting
+ public IndexCollection() {
+ this.writeIndexes = Lists.newCopyOnWriteArrayList();
+ this.searchIndex = new AtomicReference<ChangeIndex>();
+ }
+
+ /**
+ * @return the current search index version, or null if the secondary index is
+ * disabled.
+ */
+ @Nullable
+ public ChangeIndex getSearchIndex() {
+ return searchIndex.get();
+ }
+
+ public void setSearchIndex(ChangeIndex index) {
+ searchIndex.set(index);
+ }
+
+ public Collection<ChangeIndex> getWriteIndexes() {
+ return Collections.unmodifiableCollection(writeIndexes);
+ }
+
+ public synchronized void addWriteIndex(ChangeIndex index) {
+ int version = index.getSchema().getVersion();
+ for (ChangeIndex i : writeIndexes) {
+ if (i.getSchema().getVersion() == version) {
+ throw new IllegalArgumentException(
+ "Write index version " + version + " already in list");
+ }
+ }
+ writeIndexes.add(index);
+ }
+
+ public synchronized void removeWriteIndex(int version) {
+ int removeIndex = -1;
+ for (int i = 0; i < writeIndexes.size(); i++) {
+ if (writeIndexes.get(i).getSchema().getVersion() == version) {
+ removeIndex = i;
+ break;
+ }
+ }
+ if (removeIndex >= 0) {
+ writeIndexes.remove(removeIndex);
+ }
+ }
+
+ public ChangeIndex getWriteIndex(int version) {
+ for (ChangeIndex i : writeIndexes) {
+ if (i.getSchema().getVersion() == version) {
+ return i;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void stop() {
+ ChangeIndex read = searchIndex.get();
+ if (read != null) {
+ read.close();
+ }
+ for (ChangeIndex write : writeIndexes) {
+ if (write != read) {
+ write.close();
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index 5bdbd96..f46fe7c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -16,16 +16,16 @@
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.WorkQueue.Executor;
-import com.google.gerrit.server.query.change.IndexRewrite;
-import com.google.gerrit.server.query.change.IndexRewriteImpl;
-import com.google.inject.AbstractModule;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Provides;
import com.google.inject.Singleton;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
import org.eclipse.jgit.lib.Config;
@@ -35,7 +35,7 @@
* This module should not be used directly except by specific secondary indexer
* implementations (e.g. Lucene).
*/
-public class IndexModule extends AbstractModule {
+public class IndexModule extends LifecycleModule {
public enum IndexType {
SQL, LUCENE, SOLR;
}
@@ -55,8 +55,13 @@
@Override
protected void configure() {
- bind(ChangeIndexer.class).to(ChangeIndexerImpl.class);
- bind(IndexRewrite.class).to(IndexRewriteImpl.class);
+ bind(ChangeQueryRewriter.class).to(IndexRewriteImpl.class);
+ bind(IndexRewriteImpl.BasicRewritesImpl.class);
+ bind(IndexCollection.class);
+ listener().to(IndexCollection.class);
+ install(new FactoryModuleBuilder()
+ .implement(ChangeIndexer.class, ChangeIndexerImpl.class)
+ .build(ChangeIndexer.Factory.class));
}
@Provides
@@ -77,4 +82,11 @@
}
return MoreExecutors.listeningDecorator(executor);
}
+
+ @Provides
+ ChangeIndexer getChangeIndexer(
+ ChangeIndexer.Factory factory,
+ IndexCollection indexes) {
+ return factory.create(indexes);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
index 82e3aeb..d3b9e95 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
@@ -37,12 +37,4 @@
public FieldType<?> getType() {
return def.getType();
}
-
- /**
- * @return whether this predicate can only be satisfied by looking at the
- * secondary index, i.e. it cannot be expressed as a query over the DB.
- */
- public boolean isIndexOnly() {
- return false;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
new file mode 100644
index 0000000..e0c4fe9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
@@ -0,0 +1,273 @@
+// Copyright (C) 2013 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;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.gerrit.server.query.change.AndSource;
+import com.google.gerrit.server.query.change.BasicChangeRewrites;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.OrSource;
+import com.google.gerrit.server.query.change.SqlRewriterImpl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.BitSet;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+/** Rewriter that pushes boolean logic into the secondary index. */
+public class IndexRewriteImpl implements ChangeQueryRewriter {
+ /** Set of all open change statuses. */
+ public static final Set<Change.Status> OPEN_STATUSES;
+
+ /** Set of all closed change statuses. */
+ public static final Set<Change.Status> CLOSED_STATUSES;
+
+ static {
+ EnumSet<Change.Status> open = EnumSet.noneOf(Change.Status.class);
+ EnumSet<Change.Status> closed = EnumSet.noneOf(Change.Status.class);
+ for (Change.Status s : Change.Status.values()) {
+ if (s.isOpen()) {
+ open.add(s);
+ } else {
+ closed.add(s);
+ }
+ }
+ OPEN_STATUSES = Sets.immutableEnumSet(open);
+ CLOSED_STATUSES = Sets.immutableEnumSet(closed);
+ }
+
+ /**
+ * Get the set of statuses that changes matching the given predicate may have.
+ *
+ * @param in predicate
+ * @return the maximal set of statuses that any changes matching the input
+ * predicates may have, based on examining boolean and
+ * {@link ChangeStatusPredicate}s.
+ */
+ public static EnumSet<Change.Status> getPossibleStatus(Predicate<ChangeData> in) {
+ EnumSet<Change.Status> s = extractStatus(in);
+ return s != null ? s : EnumSet.allOf(Change.Status.class);
+ }
+
+ private static EnumSet<Change.Status> extractStatus(Predicate<ChangeData> in) {
+ if (in instanceof ChangeStatusPredicate) {
+ return EnumSet.of(((ChangeStatusPredicate) in).getStatus());
+ } else if (in instanceof NotPredicate) {
+ EnumSet<Status> s = extractStatus(in.getChild(0));
+ return s != null ? EnumSet.complementOf(s) : null;
+ } else if (in instanceof OrPredicate) {
+ EnumSet<Change.Status> r = null;
+ int childrenWithStatus = 0;
+ for (int i = 0; i < in.getChildCount(); i++) {
+ EnumSet<Status> c = extractStatus(in.getChild(i));
+ if (c != null) {
+ if (r == null) {
+ r = EnumSet.noneOf(Change.Status.class);
+ }
+ r.addAll(c);
+ childrenWithStatus++;
+ }
+ }
+ if (r != null && childrenWithStatus < in.getChildCount()) {
+ // At least one child supplied a status but another did not.
+ // Assume all statuses for the children that did not feed a
+ // status at this part of the tree. This matches behavior if
+ // the child was used at the root of a query.
+ return EnumSet.allOf(Change.Status.class);
+ }
+ return r;
+ } else if (in instanceof AndPredicate) {
+ EnumSet<Change.Status> r = null;
+ for (int i = 0; i < in.getChildCount(); i++) {
+ EnumSet<Change.Status> c = extractStatus(in.getChild(i));
+ if (c != null) {
+ if (r == null) {
+ r = EnumSet.allOf(Change.Status.class);
+ }
+ r.retainAll(c);
+ }
+ }
+ return r;
+ }
+ return null;
+ }
+
+ private final IndexCollection indexes;
+ private final Provider<ReviewDb> db;
+ private final BasicRewritesImpl basicRewrites;
+
+ @Inject
+ IndexRewriteImpl(IndexCollection indexes,
+ Provider<ReviewDb> db,
+ BasicRewritesImpl basicRewrites) {
+ this.indexes = indexes;
+ this.db = db;
+ this.basicRewrites = basicRewrites;
+ }
+
+ @Override
+ public Predicate<ChangeData> rewrite(Predicate<ChangeData> in) {
+ in = basicRewrites.rewrite(in);
+
+ ChangeIndex index = indexes.getSearchIndex();
+ Predicate<ChangeData> out = rewriteImpl(in, index);
+ if (in == out || out instanceof IndexPredicate) {
+ return query(out, index);
+ } else if (out == null /* cannot rewrite */) {
+ return in;
+ } else {
+ return out;
+ }
+ }
+
+ /**
+ * Rewrite a single predicate subtree.
+ *
+ * @param in predicate to rewrite.
+ * @param index index whose schema determines which fields are indexed.
+ * @return {@code null} if no part of this subtree can be queried in the
+ * index directly. {@code in} if this subtree and all its children can be
+ * queried directly in the index. Otherwise, a predicate that is
+ * semantically equivalent, with some of its subtrees wrapped to query the
+ * index directly.
+ */
+ private Predicate<ChangeData> rewriteImpl(Predicate<ChangeData> in,
+ ChangeIndex index) {
+ if (isIndexPredicate(in, index)) {
+ return in;
+ } else if (!isRewritePossible(in)) {
+ return null; // magic to indicate "in" cannot be rewritten
+ }
+
+ int n = in.getChildCount();
+ BitSet isIndexed = new BitSet(n);
+ BitSet notIndexed = new BitSet(n);
+ BitSet rewritten = new BitSet(n);
+ List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(n);
+ for (int i = 0; i < n; i++) {
+ Predicate<ChangeData> c = in.getChild(i);
+ Predicate<ChangeData> nc = rewriteImpl(c, index);
+ if (nc == c) {
+ isIndexed.set(i);
+ newChildren.add(c);
+ } else if (nc == null /* cannot rewrite c */) {
+ notIndexed.set(i);
+ newChildren.add(c);
+ } else {
+ rewritten.set(i);
+ newChildren.add(nc);
+ }
+ }
+
+ if (isIndexed.cardinality() == n) {
+ return in; // All children are indexed, leave as-is for parent.
+ } else if (notIndexed.cardinality() == n) {
+ return null; // Can't rewrite any children, so cannot rewrite in.
+ } else if (rewritten.cardinality() == n) {
+ return in.copy(newChildren); // All children were rewritten.
+ }
+ return partitionChildren(in, newChildren, isIndexed, index);
+ }
+
+ private boolean isIndexPredicate(Predicate<ChangeData> in, ChangeIndex index) {
+ if (!(in instanceof IndexPredicate)) {
+ return false;
+ }
+ IndexPredicate<ChangeData> p = (IndexPredicate<ChangeData>) in;
+ return index.getSchema().getFields().containsKey(p.getField().getName());
+ }
+
+ private Predicate<ChangeData> partitionChildren(
+ Predicate<ChangeData> in,
+ List<Predicate<ChangeData>> newChildren,
+ BitSet isIndexed,
+ ChangeIndex index) {
+ if (isIndexed.cardinality() == 1) {
+ int i = isIndexed.nextSetBit(0);
+ newChildren.add(0, query(newChildren.remove(i), index));
+ return copy(in, newChildren);
+ }
+
+ // Group all indexed predicates into a wrapped subtree.
+ List<Predicate<ChangeData>> indexed =
+ Lists.newArrayListWithCapacity(isIndexed.cardinality());
+
+ List<Predicate<ChangeData>> all =
+ Lists.newArrayListWithCapacity(
+ newChildren.size() - isIndexed.cardinality() + 1);
+
+ for (int i = 0; i < newChildren.size(); i++) {
+ Predicate<ChangeData> c = newChildren.get(i);
+ if (isIndexed.get(i)) {
+ indexed.add(c);
+ } else {
+ all.add(c);
+ }
+ }
+ all.add(0, query(in.copy(indexed), index));
+ return copy(in, all);
+ }
+
+ private Predicate<ChangeData> copy(
+ Predicate<ChangeData> in,
+ List<Predicate<ChangeData>> all) {
+ if (in instanceof AndPredicate) {
+ return new AndSource(db, all);
+ } else if (in instanceof OrPredicate) {
+ return new OrSource(all);
+ }
+ return in.copy(all);
+ }
+
+ private IndexedChangeQuery query(Predicate<ChangeData> p, ChangeIndex index) {
+ try {
+ return new IndexedChangeQuery(index, p);
+ } catch (QueryParseException e) {
+ throw new IllegalStateException(
+ "Failed to convert " + p + " to index predicate", e);
+ }
+ }
+
+ private static boolean isRewritePossible(Predicate<ChangeData> p) {
+ return p.getChildCount() > 0 && (
+ p instanceof AndPredicate
+ || p instanceof OrPredicate
+ || p instanceof NotPredicate);
+ }
+
+ static class BasicRewritesImpl extends BasicChangeRewrites {
+ private static final QueryRewriter.Definition<ChangeData, BasicRewritesImpl> mydef =
+ new QueryRewriter.Definition<ChangeData, BasicRewritesImpl>(
+ BasicRewritesImpl.class, SqlRewriterImpl.BUILDER);
+ @Inject
+ BasicRewritesImpl(Provider<ReviewDb> db) {
+ super(mydef, db);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
similarity index 83%
rename from gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
index 39eed81..175208a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/PredicateWrapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.index;
import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
@@ -35,18 +36,36 @@
* the secondary index; such predicates must also implement
* {@link ChangeDataSource} to be chosen by the query processor.
*/
-public class PredicateWrapper extends Predicate<ChangeData> implements
- ChangeDataSource {
+public class IndexedChangeQuery extends Predicate<ChangeData>
+ implements ChangeDataSource {
private final Predicate<ChangeData> pred;
private final ChangeDataSource source;
- public PredicateWrapper(ChangeIndex index, Predicate<ChangeData> pred)
+ public IndexedChangeQuery(ChangeIndex index, Predicate<ChangeData> pred)
throws QueryParseException {
this.pred = pred;
this.source = index.getSource(pred);
}
@Override
+ public int getChildCount() {
+ return 1;
+ }
+
+ @Override
+ public Predicate<ChangeData> getChild(int i) {
+ if (i == 0) {
+ return pred;
+ }
+ throw new ArrayIndexOutOfBoundsException(i);
+ }
+
+ @Override
+ public List<Predicate<ChangeData>> getChildren() {
+ return ImmutableList.of(pred);
+ }
+
+ @Override
public int getCardinality() {
return source.getCardinality();
}
@@ -115,11 +134,11 @@
public boolean equals(Object other) {
return other != null
&& getClass() == other.getClass()
- && pred.equals(((PredicateWrapper) other).pred);
+ && pred.equals(((IndexedChangeQuery) other).pred);
}
@Override
public String toString() {
- return "index(" + pred + ")";
+ return "index(" + source + ")";
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
index 3417534..8c552d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
@@ -14,7 +14,8 @@
package com.google.gerrit.server.index;
-import com.google.gerrit.server.query.change.IndexRewrite;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gerrit.server.query.change.SqlRewriterImpl;
import com.google.inject.AbstractModule;
public class NoIndexModule extends AbstractModule {
@@ -26,6 +27,6 @@
protected void configure() {
bind(ChangeIndex.class).toInstance(ChangeIndex.DISABLED);
bind(ChangeIndexer.class).toInstance(ChangeIndexer.DISABLED);
- bind(IndexRewrite.class).toInstance(IndexRewrite.DISABLED);
+ bind(ChangeQueryRewriter.class).to(SqlRewriterImpl.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
new file mode 100644
index 0000000..94d1f9c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2013 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.git;
+
+package com.google.gerrit.server.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+
+/** Specific version of a secondary index schema. */
+public class Schema<T> {
+ private final boolean release;
+ private final ImmutableMap<String, FieldDef<T, ?>> fields;
+ private int version;
+
+ protected Schema(boolean release, Iterable<FieldDef<T, ?>> fields) {
+ this(0, release, fields);
+ }
+
+ @VisibleForTesting
+ public Schema(int version, boolean release,
+ Iterable<FieldDef<T, ?>> fields) {
+ this.version = version;
+ this.release = release;
+ ImmutableMap.Builder<String, FieldDef<T, ?>> b = ImmutableMap.builder();
+ for (FieldDef<T, ?> f : fields) {
+ b.put(f.getName(), f);
+ }
+ this.fields = b.build();
+ }
+
+ public final boolean isRelease() {
+ return release;
+ }
+
+ public final int getVersion() {
+ return version;
+ }
+
+ public final ImmutableMap<String, FieldDef<T, ?>> getFields() {
+ return fields;
+ }
+
+ void setVersion(int version) {
+ this.version = version;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index ce50002..dc09a9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -87,6 +87,7 @@
init();
format();
+ appendText(velocifyFile("Footer.vm"));
if (shouldSendMessage()) {
if (fromId != null) {
final Account fromUser = args.accountCache.get(fromId).getAccount();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index a50e71c..6e5ef65 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -160,7 +160,8 @@
DashboardInfo info = new DashboardInfo(refName, path);
info.project = project;
info.definingProject = definingProject.getName();
- info.title = replace(project, config.getString("dashboard", null, "title"));
+ String query = config.getString("dashboard", null, "title");
+ info.title = replace(project, query == null ? info.path : query);
info.description = replace(project, config.getString("dashboard", null, "description"));
info.foreach = config.getString("dashboard", null, "foreach");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
index b6a08d3..6088d8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query;
+import com.google.common.collect.Lists;
import com.google.inject.name.Named;
import java.lang.annotation.Annotation;
@@ -27,7 +28,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Comparator;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
@@ -65,19 +66,13 @@
private final List<RewriteRule<T>> rewriteRules;
public Definition(Class<R> clazz, QueryBuilder<T> qb) {
- rewriteRules = new ArrayList<RewriteRule<T>>();
+ rewriteRules = Lists.newArrayList();
Class<?> c = clazz;
while (c != QueryRewriter.class) {
- final Method[] declared = c.getDeclaredMethods();
- Arrays.sort(declared, new Comparator<Method>() {
- @Override
- public int compare(Method o1, Method o2) {
- return o1.getName().compareTo(o2.getName());
- }
- });
+ Method[] declared = c.getDeclaredMethods();
for (Method m : declared) {
- final Rewrite rp = m.getAnnotation(Rewrite.class);
+ Rewrite rp = m.getAnnotation(Rewrite.class);
if ((m.getModifiers() & Modifier.ABSTRACT) != Modifier.ABSTRACT
&& (m.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC
&& rp != null) {
@@ -86,6 +81,7 @@
}
c = c.getSuperclass();
}
+ Collections.sort(rewriteRules);
}
}
@@ -340,7 +336,7 @@
}
/** Applies a rewrite rule to a Predicate. */
- protected interface RewriteRule<T> {
+ protected interface RewriteRule<T> extends Comparable<RewriteRule<T>> {
/**
* Apply a rewrite rule to the Predicate.
*
@@ -463,6 +459,15 @@
final String msg = "Cannot apply " + method.getName();
return new IllegalArgumentException(msg, e);
}
+
+ @Override
+ public int compareTo(RewriteRule<T> in) {
+ if (in instanceof MethodRewrite) {
+ return method.getName().compareTo(
+ ((MethodRewrite<T>) in).method.getName());
+ }
+ return 1;
+ }
}
private static <T> Predicate<T> removeDuplicates(Predicate<T> in) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index 7f726a7..0555fc7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -34,7 +34,8 @@
import java.util.Comparator;
import java.util.List;
-class AndSource extends AndPredicate<ChangeData> implements ChangeDataSource {
+public class AndSource extends AndPredicate<ChangeData>
+ implements ChangeDataSource {
private static final Comparator<Predicate<ChangeData>> CMP =
new Comparator<Predicate<ChangeData>>() {
@Override
@@ -75,7 +76,8 @@
private final Provider<ReviewDb> db;
private int cardinality = -1;
- AndSource(Provider<ReviewDb> db, Collection<? extends Predicate<ChangeData>> that) {
+ public AndSource(Provider<ReviewDb> db,
+ Collection<? extends Predicate<ChangeData>> that) {
super(sort(that));
this.db = db;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
new file mode 100644
index 0000000..1d2c9d2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 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.query.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.query.IntPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+public abstract class BasicChangeRewrites extends QueryRewriter<ChangeData> {
+ protected static final ChangeQueryBuilder BUILDER = new ChangeQueryBuilder(
+ new ChangeQueryBuilder.Arguments( //
+ new InvalidProvider<ReviewDb>(), //
+ new InvalidProvider<ChangeQueryRewriter>(), //
+ null, null, null, null, null, //
+ null, null, null, null, null), null);
+
+ protected final Provider<ReviewDb> dbProvider;
+
+ protected BasicChangeRewrites(
+ Definition<ChangeData, ? extends QueryRewriter<ChangeData>> def,
+ Provider<ReviewDb> dbProvider) {
+ super(def);
+ this.dbProvider = dbProvider;
+ }
+
+ @Rewrite("-status:open")
+ @NoCostComputation
+ public Predicate<ChangeData> r00_notOpen() {
+ return ChangeStatusPredicate.closed(dbProvider);
+ }
+
+ @Rewrite("-status:closed")
+ @NoCostComputation
+ public Predicate<ChangeData> r00_notClosed() {
+ return ChangeStatusPredicate.open(dbProvider);
+ }
+
+ @SuppressWarnings("unchecked")
+ @NoCostComputation
+ @Rewrite("-status:merged")
+ public Predicate<ChangeData> r00_notMerged() {
+ return or(ChangeStatusPredicate.open(dbProvider),
+ new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED));
+ }
+
+ @SuppressWarnings("unchecked")
+ @NoCostComputation
+ @Rewrite("-status:abandoned")
+ public Predicate<ChangeData> r00_notAbandoned() {
+ return or(ChangeStatusPredicate.open(dbProvider),
+ new ChangeStatusPredicate(dbProvider, Change.Status.MERGED));
+ }
+
+ @SuppressWarnings("unchecked")
+ @NoCostComputation
+ @Rewrite("sortkey_before:z A=(age:*)")
+ public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) {
+ String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE);
+ return and(new SortKeyPredicate.Before(dbProvider, cut), a);
+ }
+
+ @NoCostComputation
+ @Rewrite("A=(limit:*) B=(limit:*)")
+ public Predicate<ChangeData> r00_smallestLimit(
+ @Named("A") IntPredicate<ChangeData> a,
+ @Named("B") IntPredicate<ChangeData> b) {
+ return a.intValue() <= b.intValue() ? a : b;
+ }
+
+ @NoCostComputation
+ @Rewrite("A=(sortkey_before:*) B=(sortkey_before:*)")
+ public Predicate<ChangeData> r00_oldestSortKey(
+ @Named("A") SortKeyPredicate.Before a,
+ @Named("B") SortKeyPredicate.Before b) {
+ return a.getValue().compareTo(b.getValue()) <= 0 ? a : b;
+ }
+
+ @NoCostComputation
+ @Rewrite("A=(sortkey_after:*) B=(sortkey_after:*)")
+ public Predicate<ChangeData> r00_newestSortKey(
+ @Named("A") SortKeyPredicate.After a, @Named("B") SortKeyPredicate.After b) {
+ return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
+ }
+
+ private static final class InvalidProvider<T> implements Provider<T> {
+ @Override
+ public T get() {
+ throw new OutOfScopeException("Not available at init");
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 4f68185..52f299c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -33,6 +33,7 @@
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache;
@@ -110,7 +111,8 @@
new QueryBuilder.Definition<ChangeData, ChangeQueryBuilder>(
ChangeQueryBuilder.class);
- static class Arguments {
+ @VisibleForTesting
+ public static class Arguments {
final Provider<ReviewDb> dbProvider;
final Provider<ChangeQueryRewriter> rewriter;
final IdentifiedUser.GenericFactory userFactory;
@@ -122,10 +124,11 @@
final PatchListCache patchListCache;
final GitRepositoryManager repoManager;
final ProjectCache projectCache;
- final ChangeIndex index;
+ final IndexCollection indexes;
@Inject
- Arguments(Provider<ReviewDb> dbProvider,
+ @VisibleForTesting
+ public Arguments(Provider<ReviewDb> dbProvider,
Provider<ChangeQueryRewriter> rewriter,
IdentifiedUser.GenericFactory userFactory,
CapabilityControl.Factory capabilityControlFactory,
@@ -136,7 +139,7 @@
PatchListCache patchListCache,
GitRepositoryManager repoManager,
ProjectCache projectCache,
- ChangeIndex index) {
+ IndexCollection indexes) {
this.dbProvider = dbProvider;
this.rewriter = rewriter;
this.userFactory = userFactory;
@@ -148,7 +151,7 @@
this.patchListCache = patchListCache;
this.repoManager = repoManager;
this.projectCache = projectCache;
- this.index = index;
+ this.indexes = indexes;
}
}
@@ -161,7 +164,7 @@
private boolean allowFileRegex;
@Inject
- ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
+ public ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
super(mydef);
this.args = args;
this.currentUser = currentUser;
@@ -203,10 +206,11 @@
@Operator
public Predicate<ChangeData> comment(String value) throws QueryParseException {
- if (args.index == ChangeIndex.DISABLED) {
+ ChangeIndex index = args.indexes.getSearchIndex();
+ if (index == null) {
throw error("secondary index must be enabled for comment:" + value);
}
- return new CommentPredicate(args.dbProvider, args.index, value);
+ return new CommentPredicate(args.dbProvider, index, value);
}
@Operator
@@ -320,13 +324,13 @@
@Operator
public Predicate<ChangeData> file(String file) throws QueryParseException {
if (file.startsWith("^")) {
- if (allowFileRegex || args.index != ChangeIndex.DISABLED) {
+ if (allowFileRegex || args.indexes.getSearchIndex() != null) {
return new RegexFilePredicate(args.dbProvider, args.patchListCache, file);
} else {
throw error("secondary index must be enabled for file:" + file);
}
} else {
- if (args.index == ChangeIndex.DISABLED) {
+ if (args.indexes.getSearchIndex() == null) {
throw error("secondary index must be enabled for file:" + file);
}
return new EqualsFilePredicate(args.dbProvider, args.patchListCache, file);
@@ -389,11 +393,12 @@
@Operator
public Predicate<ChangeData> message(String text) throws QueryParseException {
- if (args.index == ChangeIndex.DISABLED) {
+ ChangeIndex index = args.indexes.getSearchIndex();
+ if (index == null) {
throw error("secondary index must be enabled for message:" + text);
}
- return new MessagePredicate(args.dbProvider, args.index, text);
+ return new MessagePredicate(args.dbProvider, index, text);
}
@Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index 2988021..1d6de6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2013 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.
@@ -14,701 +14,8 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryRewriter;
-import com.google.gerrit.server.query.RewritePredicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Inject;
-import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
-import com.google.inject.name.Named;
-import java.util.Collection;
-
-public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
- private static final QueryRewriter.Definition<ChangeData, ChangeQueryRewriter> mydef =
- new QueryRewriter.Definition<ChangeData, ChangeQueryRewriter>(
- ChangeQueryRewriter.class, new ChangeQueryBuilder(
- new ChangeQueryBuilder.Arguments( //
- new InvalidProvider<ReviewDb>(), //
- new InvalidProvider<ChangeQueryRewriter>(), //
- null, null, null, null, null, //
- null, null, null, null, null), null));
-
- private final Provider<ReviewDb> dbProvider;
- private final IndexRewrite indexRewrite;
-
- @Inject
- ChangeQueryRewriter(Provider<ReviewDb> dbProvider,
- IndexRewrite indexRewrite) {
- super(mydef);
- this.dbProvider = dbProvider;
- this.indexRewrite = indexRewrite;
- }
-
- @Override
- public Predicate<ChangeData> and(Collection<? extends Predicate<ChangeData>> l) {
- return hasSource(l) ? new AndSource(dbProvider, l) : super.and(l);
- }
-
- @Override
- public Predicate<ChangeData> or(Collection<? extends Predicate<ChangeData>> l) {
- return hasSource(l) ? new OrSource(l) : super.or(l);
- }
-
- @Override
- public Predicate<ChangeData> preRewrite(Predicate<ChangeData> in) {
- return indexRewrite.rewrite(in);
- }
-
- @Rewrite("-status:open")
- @NoCostComputation
- public Predicate<ChangeData> r00_notOpen() {
- return ChangeStatusPredicate.closed(dbProvider);
- }
-
- @Rewrite("-status:closed")
- @NoCostComputation
- public Predicate<ChangeData> r00_notClosed() {
- return ChangeStatusPredicate.open(dbProvider);
- }
-
- @SuppressWarnings("unchecked")
- @NoCostComputation
- @Rewrite("-status:merged")
- public Predicate<ChangeData> r00_notMerged() {
- return or(ChangeStatusPredicate.open(dbProvider),
- new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED));
- }
-
- @SuppressWarnings("unchecked")
- @NoCostComputation
- @Rewrite("-status:abandoned")
- public Predicate<ChangeData> r00_notAbandoned() {
- return or(ChangeStatusPredicate.open(dbProvider),
- new ChangeStatusPredicate(dbProvider, Change.Status.MERGED));
- }
-
- @SuppressWarnings("unchecked")
- @NoCostComputation
- @Rewrite("sortkey_before:z A=(age:*)")
- public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) {
- String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE);
- return and(new SortKeyPredicate.Before(dbProvider, cut), a);
- }
-
- @NoCostComputation
- @Rewrite("A=(limit:*) B=(limit:*)")
- public Predicate<ChangeData> r00_smallestLimit(
- @Named("A") IntPredicate<ChangeData> a,
- @Named("B") IntPredicate<ChangeData> b) {
- return a.intValue() <= b.intValue() ? a : b;
- }
-
- @NoCostComputation
- @Rewrite("A=(sortkey_before:*) B=(sortkey_before:*)")
- public Predicate<ChangeData> r00_oldestSortKey(
- @Named("A") SortKeyPredicate.Before a,
- @Named("B") SortKeyPredicate.Before b) {
- return a.getValue().compareTo(b.getValue()) <= 0 ? a : b;
- }
-
- @NoCostComputation
- @Rewrite("A=(sortkey_after:*) B=(sortkey_after:*)")
- public Predicate<ChangeData> r00_newestSortKey(
- @Named("A") SortKeyPredicate.After a, @Named("B") SortKeyPredicate.After b) {
- return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
- }
-
- @Rewrite("status:open P=(project:*) B=(ref:*)")
- public Predicate<ChangeData> r05_byBranchOpen(
- @Named("P") final ProjectPredicate p,
- @Named("B") final RefPredicate b) {
- return new ChangeSource(500) {
- @Override
- ResultSet<Change> scan(ChangeAccess a)
- throws OrmException {
- return a.byBranchOpenAll(
- new Branch.NameKey(p.getValueKey(), b.getValue()));
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen()
- && p.match(cd)
- && b.match(cd);
- }
- };
- }
-
- @Rewrite("status:merged P=(project:*) B=(ref:*) S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r05_byBranchMergedPrev(
- @Named("P") final ProjectPredicate p,
- @Named("B") final RefPredicate b,
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byBranchClosedPrev(Change.Status.MERGED.getCode(), //
- new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && p.match(cd) //
- && b.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:merged P=(project:*) B=(ref:*) S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r05_byBranchMergedNext(
- @Named("P") final ProjectPredicate p,
- @Named("B") final RefPredicate b,
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byBranchClosedNext(Change.Status.MERGED.getCode(), //
- new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && p.match(cd) //
- && b.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectOpenPrev(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(500, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectOpenPrev(p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() //
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:open P=(project:*) S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectOpenNext(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(500, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectOpenNext(p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() //
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:merged P=(project:*) S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectMergedPrev(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectClosedPrev(Change.Status.MERGED.getCode(), //
- p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:merged P=(project:*) S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectMergedNext(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectClosedNext(Change.Status.MERGED.getCode(), //
- p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:abandoned P=(project:*) S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectAbandonedPrev(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectClosedPrev(Change.Status.ABANDONED.getCode(), //
- p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:abandoned P=(project:*) S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectAbandonedNext(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectClosedNext(Change.Status.ABANDONED.getCode(), //
- p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:open S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byOpenPrev(
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(2000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allOpenPrev(key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:open S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byOpenNext(
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(2000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allOpenNext(key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:merged S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byMergedPrev(
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(50000, s.getValue(), l.intValue()) {
- {
- init("r20_byMergedPrev", s, l);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allClosedPrev(Change.Status.MERGED.getCode(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:merged S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byMergedNext(
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(50000, s.getValue(), l.intValue()) {
- {
- init("r20_byMergedNext", s, l);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allClosedNext(Change.Status.MERGED.getCode(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:abandoned S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byAbandonedPrev(
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(50000, s.getValue(), l.intValue()) {
- {
- init("r20_byAbandonedPrev", s, l);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allClosedPrev(Change.Status.ABANDONED.getCode(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
- && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:abandoned S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byAbandonedNext(
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(50000, s.getValue(), l.intValue()) {
- {
- init("r20_byAbandonedNext", s, l);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allClosedNext(Change.Status.ABANDONED.getCode(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
- && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byClosedPrev(
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l));
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byClosedNext(
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l));
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:open O=(owner:*)")
- public Predicate<ChangeData> r25_byOwnerOpen(
- @Named("O") final OwnerPredicate o) {
- return new ChangeSource(50) {
- {
- init("r25_byOwnerOpen", o);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return a.byOwnerOpen(o.getAccountId());
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() && o.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:closed O=(owner:*)")
- public Predicate<ChangeData> r25_byOwnerClosed(
- @Named("O") final OwnerPredicate o) {
- return new ChangeSource(5000) {
- {
- init("r25_byOwnerClosed", o);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return a.byOwnerClosedAll(o.getAccountId());
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isClosed() && o.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("O=(owner:*)")
- public Predicate<ChangeData> r26_byOwner(@Named("O") OwnerPredicate o) {
- return or(r25_byOwnerOpen(o), r25_byOwnerClosed(o));
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:open R=(reviewer:*)")
- public Predicate<ChangeData> r30_byReviewerOpen(
- @Named("R") final ReviewerPredicate r) {
- return new Source() {
- {
- init("r30_byReviewerOpen", r);
- }
-
- @Override
- public ResultSet<ChangeData> read() throws OrmException {
- return ChangeDataResultSet.patchSetApproval(dbProvider.get()
- .patchSetApprovals().openByUser(r.getAccountId()));
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- Change change = cd.change(dbProvider);
- return change != null && change.getStatus().isOpen() && r.match(cd);
- }
-
- @Override
- public int getCardinality() {
- return 50;
- }
-
- @Override
- public int getCost() {
- return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:closed R=(reviewer:*)")
- public Predicate<ChangeData> r30_byReviewerClosed(
- @Named("R") final ReviewerPredicate r) {
- return new Source() {
- {
- init("r30_byReviewerClosed", r);
- }
-
- @Override
- public ResultSet<ChangeData> read() throws OrmException {
- return ChangeDataResultSet.patchSetApproval(dbProvider.get()
- .patchSetApprovals().closedByUserAll(r.getAccountId()));
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- Change change = cd.change(dbProvider);
- return change != null && change.getStatus().isClosed() && r.match(cd);
- }
-
- @Override
- public int getCardinality() {
- return 5000;
- }
-
- @Override
- public int getCost() {
- return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("R=(reviewer:*)")
- public Predicate<ChangeData> r31_byReviewer(
- @Named("R") final ReviewerPredicate r) {
- return or(r30_byReviewerOpen(r), r30_byReviewerClosed(r));
- }
-
- @Rewrite("status:submitted")
- public Predicate<ChangeData> r99_allSubmitted() {
- return new ChangeSource(50) {
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return a.allSubmitted();
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.SUBMITTED;
- }
- };
- }
-
- @Rewrite("P=(project:*)")
- public Predicate<ChangeData> r99_byProject(
- @Named("P") final ProjectPredicate p) {
- return new ChangeSource(1000000) {
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return a.byProject(p.getValueKey());
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return p.match(cd);
- }
- };
- }
-
- private static boolean hasSource(Collection<? extends Predicate<ChangeData>> l) {
- for (Predicate<ChangeData> p : l) {
- if (p instanceof ChangeDataSource) {
- return true;
- }
- }
- return false;
- }
-
- private abstract static class Source extends RewritePredicate<ChangeData>
- implements ChangeDataSource {
- @Override
- public boolean hasChange() {
- return false;
- }
- }
-
- private abstract class ChangeSource extends Source {
- private final int cardinality;
-
- ChangeSource(int card) {
- this.cardinality = card;
- }
-
- abstract ResultSet<Change> scan(ChangeAccess a) throws OrmException;
-
- @Override
- public ResultSet<ChangeData> read() throws OrmException {
- return ChangeDataResultSet.change(scan(dbProvider.get().changes()));
- }
-
- @Override
- public boolean hasChange() {
- return true;
- }
-
- @Override
- public int getCardinality() {
- return cardinality;
- }
-
- @Override
- public int getCost() {
- return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality());
- }
- }
-
- private abstract class PaginatedSource extends ChangeSource implements
- Paginated {
- private final String startKey;
- private final int limit;
-
- PaginatedSource(int card, String start, int lim) {
- super(card);
- this.startKey = start;
- this.limit = lim;
- }
-
- @Override
- public int limit() {
- return limit;
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return scan(a, startKey, limit);
- }
-
- @Override
- public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
- return ChangeDataResultSet.change(scan(dbProvider.get().changes(), //
- last.change(dbProvider).getSortKey(), //
- limit));
- }
-
- abstract ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException;
- }
-
- private static final class InvalidProvider<T> implements Provider<T> {
- @Override
- public T get() {
- throw new OutOfScopeException("Not available at init");
- }
- }
+public interface ChangeQueryRewriter {
+ Predicate<ChangeData> rewrite(Predicate<ChangeData> in);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 6d08db0..d5d6a92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -47,7 +47,7 @@
VALUES = values.build();
}
- static Predicate<ChangeData> open(Provider<ReviewDb> dbProvider) {
+ public static Predicate<ChangeData> open(Provider<ReviewDb> dbProvider) {
List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
for (final Change.Status e : Change.Status.values()) {
if (e.isOpen()) {
@@ -57,7 +57,7 @@
return r.size() == 1 ? r.get(0) : or(r);
}
- static Predicate<ChangeData> closed(Provider<ReviewDb> dbProvider) {
+ public static Predicate<ChangeData> closed(Provider<ReviewDb> dbProvider) {
List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
for (final Change.Status e : Change.Status.values()) {
if (e.isClosed()) {
@@ -83,7 +83,7 @@
this.status = status;
}
- Change.Status getStatus() {
+ public Change.Status getStatus() {
return status;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
index 1437dad..05d7573 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -55,9 +55,4 @@
public int getCost() {
return 1;
}
-
- @Override
- public boolean isIndexOnly() {
- return true;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
index 1601bef..002dc99 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -54,9 +54,4 @@
public int getCost() {
return 1;
}
-
- @Override
- public boolean isIndexOnly() {
- return true;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java
deleted file mode 100644
index 88c646b..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewrite.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2013 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.query.change;
-
-import com.google.gerrit.server.query.Predicate;
-
-public interface IndexRewrite {
- /** Instance indicating secondary index is disabled. */
- public static final IndexRewrite DISABLED = new IndexRewrite() {
- @Override
- public Predicate<ChangeData> rewrite(Predicate<ChangeData> in) {
- return in;
- }
- };
-
- /**
- * Rewrite a predicate to push as much boolean logic as possible into the
- * secondary index query system.
- *
- * @param in predicate to rewrite.
- * @return a predicate with some subtrees replaced with predicates that are
- * also sources that query the index directly.
- */
- public Predicate<ChangeData> rewrite(Predicate<ChangeData> in);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java
deleted file mode 100644
index 51f6208..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IndexRewriteImpl.java
+++ /dev/null
@@ -1,222 +0,0 @@
-// Copyright (C) 2013 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.query.change;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.index.PredicateWrapper;
-import com.google.gerrit.server.query.AndPredicate;
-import com.google.gerrit.server.query.NotPredicate;
-import com.google.gerrit.server.query.OrPredicate;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.inject.Inject;
-
-import java.util.BitSet;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Set;
-
-/** Rewriter that pushes boolean logic into the secondary index. */
-public class IndexRewriteImpl implements IndexRewrite {
- /** Set of all open change statuses. */
- public static final Set<Change.Status> OPEN_STATUSES;
-
- /** Set of all closed change statuses. */
- public static final Set<Change.Status> CLOSED_STATUSES;
-
- static {
- EnumSet<Change.Status> open = EnumSet.noneOf(Change.Status.class);
- EnumSet<Change.Status> closed = EnumSet.noneOf(Change.Status.class);
- for (Change.Status s : Change.Status.values()) {
- if (s.isOpen()) {
- open.add(s);
- } else {
- closed.add(s);
- }
- }
- OPEN_STATUSES = Sets.immutableEnumSet(open);
- CLOSED_STATUSES = Sets.immutableEnumSet(closed);
- }
-
- /**
- * Get the set of statuses that changes matching the given predicate may have.
- *
- * @param in predicate
- * @return the maximal set of statuses that any changes matching the input
- * predicates may have, based on examining boolean and
- * {@link ChangeStatusPredicate}s.
- */
- public static EnumSet<Change.Status> getPossibleStatus(Predicate<ChangeData> in) {
- if (in instanceof ChangeStatusPredicate) {
- return EnumSet.of(((ChangeStatusPredicate) in).getStatus());
- } else if (in instanceof NotPredicate) {
- return EnumSet.complementOf(getPossibleStatus(in.getChild(0)));
- } else if (in instanceof OrPredicate) {
- EnumSet<Change.Status> s = EnumSet.noneOf(Change.Status.class);
- for (int i = 0; i < in.getChildCount(); i++) {
- s.addAll(getPossibleStatus(in.getChild(i)));
- }
- return s;
- } else if (in instanceof AndPredicate) {
- EnumSet<Change.Status> s = EnumSet.allOf(Change.Status.class);
- for (int i = 0; i < in.getChildCount(); i++) {
- s.retainAll(getPossibleStatus(in.getChild(i)));
- }
- return s;
- } else if (in.getChildCount() == 0) {
- return EnumSet.allOf(Change.Status.class);
- } else {
- throw new IllegalStateException(
- "Invalid predicate type in change index query: " + in.getClass());
- }
- }
-
- private final ChangeIndex index;
-
- @Inject
- IndexRewriteImpl(ChangeIndex index) {
- this.index = index;
- }
-
- @Override
- public Predicate<ChangeData> rewrite(Predicate<ChangeData> in) {
- Predicate<ChangeData> out = rewriteImpl(in);
- if (out == null) {
- return in;
- } else if (out == in) {
- return wrap(out);
- } else {
- return out;
- }
- }
-
- /**
- * Rewrite a single predicate subtree.
- *
- * @param in predicate to rewrite.
- * @return {@code null} if no part of this subtree can be queried in the
- * index directly. {@code in} if this subtree and all its children can be
- * queried directly in the index. Otherwise, a predicate that is
- * semantically equivalent, with some of its subtrees wrapped to query the
- * index directly.
- */
- private Predicate<ChangeData> rewriteImpl(Predicate<ChangeData> in) {
- if (in instanceof IndexPredicate) {
- return in;
- }
- if (!isBoolean(in)) {
- return null;
- }
- int n = in.getChildCount();
- BitSet toKeep = new BitSet(n);
- BitSet toWrap = new BitSet(n);
- BitSet rewritten = new BitSet(n);
- List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(n);
- for (int i = 0; i < n; i++) {
- Predicate<ChangeData> c = in.getChild(i);
- Predicate<ChangeData> nc = rewriteImpl(c);
- if (nc == null) {
- toKeep.set(i);
- newChildren.add(c);
- } else if (nc == c) {
- toWrap.set(i);
- newChildren.add(nc);
- } else {
- rewritten.set(i);
- newChildren.add(nc);
- }
- }
- if (toKeep.cardinality() == n) {
- return null; // Can't rewrite any children.
- }
- if (rewritten.cardinality() == n) {
- // All children were partially, but not fully, rewritten.
- return in.copy(newChildren);
- }
- if (toWrap.cardinality() == n) {
- // All children can be fully rewritten, push work to parent.
- return in;
- }
- return partitionChildren(in, newChildren, toWrap);
- }
-
-
- private Predicate<ChangeData> partitionChildren(Predicate<ChangeData> in,
- List<Predicate<ChangeData>> newChildren, BitSet toWrap) {
- if (toWrap.cardinality() == 1) {
- int i = toWrap.nextSetBit(0);
- newChildren.set(i, wrap(newChildren.get(i)));
- return in.copy(newChildren);
- }
-
- // Group all toWrap predicates into a wrapped subtree and place it as a
- // sibling of the non-/partially-wrapped predicates. Assumes partitioning
- // the children into arbitrary subtrees of the same type is logically
- // equivalent to having them as siblings.
- List<Predicate<ChangeData>> wrapped = Lists.newArrayListWithCapacity(
- toWrap.cardinality());
- List<Predicate<ChangeData>> all = Lists.newArrayListWithCapacity(
- newChildren.size() - toWrap.cardinality() + 1);
- for (int i = 0; i < newChildren.size(); i++) {
- Predicate<ChangeData> child = newChildren.get(i);
- if (toWrap.get(i)) {
- wrapped.add(child);
- if (allNonIndexOnly(child)) {
- // Duplicate non-index-only predicate subtrees alongside the wrapped
- // subtrees so they can provide index hints to the DB-based rewriter.
- all.add(child);
- }
- } else {
- all.add(child);
- }
- }
- all.add(wrap(in.copy(wrapped)));
- return in.copy(all);
- }
-
- private static boolean allNonIndexOnly(Predicate<ChangeData> p) {
- if (p instanceof IndexPredicate) {
- return !((IndexPredicate<ChangeData>) p).isIndexOnly();
- }
- if (isBoolean(p)) {
- for (int i = 0; i < p.getChildCount(); i++) {
- if (!allNonIndexOnly(p.getChild(i))) {
- return false;
- }
- }
- return true;
- } else {
- return true;
- }
- }
-
- private PredicateWrapper wrap(Predicate<ChangeData> p) {
- try {
- return new PredicateWrapper(index, p);
- } catch (QueryParseException e) {
- throw new IllegalStateException(
- "Failed to convert " + p + " to index predicate", e);
- }
- }
-
- private static boolean isBoolean(Predicate<ChangeData> p) {
- return p instanceof AndPredicate || p instanceof OrPredicate
- || p instanceof NotPredicate;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 514c29f..62a3876 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -59,9 +59,4 @@
public int getCost() {
return 1;
}
-
- @Override
- public boolean isIndexOnly() {
- return true;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
index ec5195b..4f36777 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
@@ -25,10 +25,10 @@
import java.util.Collection;
import java.util.HashSet;
-class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
+public class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
private int cardinality = -1;
- OrSource(final Collection<? extends Predicate<ChangeData>> that) {
+ public OrSource(Collection<? extends Predicate<ChangeData>> that) {
super(that);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SqlRewriterImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SqlRewriterImpl.java
new file mode 100644
index 0000000..78b3c95
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SqlRewriterImpl.java
@@ -0,0 +1,631 @@
+// Copyright (C) 2009 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.query.change;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.IntPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.gerrit.server.query.RewritePredicate;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+import java.util.Collection;
+
+public class SqlRewriterImpl extends BasicChangeRewrites
+ implements ChangeQueryRewriter {
+ private static final QueryRewriter.Definition<ChangeData, SqlRewriterImpl> mydef =
+ new QueryRewriter.Definition<ChangeData, SqlRewriterImpl>(
+ SqlRewriterImpl.class, BUILDER);
+
+ @Inject
+ SqlRewriterImpl(Provider<ReviewDb> dbProvider) {
+ super(mydef, dbProvider);
+ }
+
+ @Override
+ public Predicate<ChangeData> and(Collection<? extends Predicate<ChangeData>> l) {
+ return hasSource(l) ? new AndSource(dbProvider, l) : super.and(l);
+ }
+
+ @Override
+ public Predicate<ChangeData> or(Collection<? extends Predicate<ChangeData>> l) {
+ return hasSource(l) ? new OrSource(l) : super.or(l);
+ }
+
+ @Rewrite("status:open P=(project:*) B=(ref:*)")
+ public Predicate<ChangeData> r05_byBranchOpen(
+ @Named("P") final ProjectPredicate p,
+ @Named("B") final RefPredicate b) {
+ return new ChangeSource(500) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a)
+ throws OrmException {
+ return a.byBranchOpenAll(
+ new Branch.NameKey(p.getValueKey(), b.getValue()));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen()
+ && p.match(cd)
+ && b.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:merged P=(project:*) B=(ref:*) S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r05_byBranchMergedPrev(
+ @Named("P") final ProjectPredicate p,
+ @Named("B") final RefPredicate b,
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byBranchClosedPrev(Change.Status.MERGED.getCode(), //
+ new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && p.match(cd) //
+ && b.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:merged P=(project:*) B=(ref:*) S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r05_byBranchMergedNext(
+ @Named("P") final ProjectPredicate p,
+ @Named("B") final RefPredicate b,
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byBranchClosedNext(Change.Status.MERGED.getCode(), //
+ new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && p.match(cd) //
+ && b.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectOpenPrev(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(500, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectOpenPrev(p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() //
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:open P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectOpenNext(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(500, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectOpenNext(p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() //
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:merged P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectMergedPrev(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectClosedPrev(Change.Status.MERGED.getCode(), //
+ p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:merged P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectMergedNext(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectClosedNext(Change.Status.MERGED.getCode(), //
+ p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:abandoned P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectAbandonedPrev(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectClosedPrev(Change.Status.ABANDONED.getCode(), //
+ p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:abandoned P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectAbandonedNext(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectClosedNext(Change.Status.ABANDONED.getCode(), //
+ p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:open S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byOpenPrev(
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allOpenPrev(key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:open S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byOpenNext(
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allOpenNext(key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:merged S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byMergedPrev(
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+ {
+ init("r20_byMergedPrev", s, l);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allClosedPrev(Change.Status.MERGED.getCode(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:merged S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byMergedNext(
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+ {
+ init("r20_byMergedNext", s, l);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allClosedNext(Change.Status.MERGED.getCode(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:abandoned S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byAbandonedPrev(
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+ {
+ init("r20_byAbandonedPrev", s, l);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allClosedPrev(Change.Status.ABANDONED.getCode(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+ && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:abandoned S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byAbandonedNext(
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+ {
+ init("r20_byAbandonedNext", s, l);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allClosedNext(Change.Status.ABANDONED.getCode(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+ && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byClosedPrev(
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byClosedNext(
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:open O=(owner:*)")
+ public Predicate<ChangeData> r25_byOwnerOpen(
+ @Named("O") final OwnerPredicate o) {
+ return new ChangeSource(50) {
+ {
+ init("r25_byOwnerOpen", o);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return a.byOwnerOpen(o.getAccountId());
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() && o.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:closed O=(owner:*)")
+ public Predicate<ChangeData> r25_byOwnerClosed(
+ @Named("O") final OwnerPredicate o) {
+ return new ChangeSource(5000) {
+ {
+ init("r25_byOwnerClosed", o);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return a.byOwnerClosedAll(o.getAccountId());
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isClosed() && o.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("O=(owner:*)")
+ public Predicate<ChangeData> r26_byOwner(@Named("O") OwnerPredicate o) {
+ return or(r25_byOwnerOpen(o), r25_byOwnerClosed(o));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:open R=(reviewer:*)")
+ public Predicate<ChangeData> r30_byReviewerOpen(
+ @Named("R") final ReviewerPredicate r) {
+ return new Source() {
+ {
+ init("r30_byReviewerOpen", r);
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ return ChangeDataResultSet.patchSetApproval(dbProvider.get()
+ .patchSetApprovals().openByUser(r.getAccountId()));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ Change change = cd.change(dbProvider);
+ return change != null && change.getStatus().isOpen() && r.match(cd);
+ }
+
+ @Override
+ public int getCardinality() {
+ return 50;
+ }
+
+ @Override
+ public int getCost() {
+ return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:closed R=(reviewer:*)")
+ public Predicate<ChangeData> r30_byReviewerClosed(
+ @Named("R") final ReviewerPredicate r) {
+ return new Source() {
+ {
+ init("r30_byReviewerClosed", r);
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ return ChangeDataResultSet.patchSetApproval(dbProvider.get()
+ .patchSetApprovals().closedByUserAll(r.getAccountId()));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ Change change = cd.change(dbProvider);
+ return change != null && change.getStatus().isClosed() && r.match(cd);
+ }
+
+ @Override
+ public int getCardinality() {
+ return 5000;
+ }
+
+ @Override
+ public int getCost() {
+ return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("R=(reviewer:*)")
+ public Predicate<ChangeData> r31_byReviewer(
+ @Named("R") final ReviewerPredicate r) {
+ return or(r30_byReviewerOpen(r), r30_byReviewerClosed(r));
+ }
+
+ @Rewrite("status:submitted")
+ public Predicate<ChangeData> r99_allSubmitted() {
+ return new ChangeSource(50) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return a.allSubmitted();
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.SUBMITTED;
+ }
+ };
+ }
+
+ @Rewrite("P=(project:*)")
+ public Predicate<ChangeData> r99_byProject(
+ @Named("P") final ProjectPredicate p) {
+ return new ChangeSource(1000000) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return a.byProject(p.getValueKey());
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return p.match(cd);
+ }
+ };
+ }
+
+ private static boolean hasSource(Collection<? extends Predicate<ChangeData>> l) {
+ for (Predicate<ChangeData> p : l) {
+ if (p instanceof ChangeDataSource) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private abstract static class Source extends RewritePredicate<ChangeData>
+ implements ChangeDataSource {
+ @Override
+ public boolean hasChange() {
+ return false;
+ }
+ }
+
+ private abstract class ChangeSource extends Source {
+ private final int cardinality;
+
+ ChangeSource(int card) {
+ this.cardinality = card;
+ }
+
+ abstract ResultSet<Change> scan(ChangeAccess a) throws OrmException;
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ return ChangeDataResultSet.change(scan(dbProvider.get().changes()));
+ }
+
+ @Override
+ public boolean hasChange() {
+ return true;
+ }
+
+ @Override
+ public int getCardinality() {
+ return cardinality;
+ }
+
+ @Override
+ public int getCost() {
+ return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality());
+ }
+ }
+
+ private abstract class PaginatedSource extends ChangeSource implements
+ Paginated {
+ private final String startKey;
+ private final int limit;
+
+ PaginatedSource(int card, String start, int lim) {
+ super(card);
+ this.startKey = start;
+ this.limit = lim;
+ }
+
+ @Override
+ public int limit() {
+ return limit;
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return scan(a, startKey, limit);
+ }
+
+ @Override
+ public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
+ return ChangeDataResultSet.change(scan(dbProvider.get().changes(), //
+ last.change(dbProvider).getSortKey(), //
+ limit));
+ }
+
+ abstract ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException;
+ }
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm
new file mode 100644
index 0000000..28f29fd
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm
@@ -0,0 +1,33 @@
+## Copyright (C) 2013 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.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## Gerrit will use templates ending in ".vm" but will ignore templates ending
+## in ".vm.example". If a .vm template does not exist, the default internal
+## gerrit template which is the same as the .vm.example will be used. If you
+## want to override the default template, copy the .vm.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The Footer.vm template will determine the contents of the footer text
+## appended to the end of all outgoing emails after the ChangeFooter and
+## CommentFooter.
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
similarity index 67%
rename from gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java
rename to gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
index a754524..e91c3a0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/IndexRewriteTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.query.change;
+package com.google.gerrit.server.index;
import static com.google.gerrit.reviewdb.client.Change.Status.ABANDONED;
import static com.google.gerrit.reviewdb.client.Change.Status.DRAFT;
@@ -23,13 +23,15 @@
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.PredicateWrapper;
import com.google.gerrit.server.query.AndPredicate;
import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gerrit.server.query.OrPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.AndSource;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.OrSource;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
@@ -40,7 +42,22 @@
@SuppressWarnings("unchecked")
public class IndexRewriteTest extends TestCase {
+ private static Schema<ChangeData> V1 = new Schema<ChangeData>(
+ 1, false, ImmutableList.<FieldDef<ChangeData, ?>> of(
+ ChangeField.STATUS));
+
+ private static Schema<ChangeData> V2 = new Schema<ChangeData>(
+ 2, false, ImmutableList.of(
+ ChangeField.STATUS,
+ ChangeField.FILE));
+
private static class DummyIndex implements ChangeIndex {
+ private final Schema<ChangeData> schema;
+
+ private DummyIndex(Schema<ChangeData> schema) {
+ this.schema = schema;
+ }
+
@Override
public ListenableFuture<Void> insert(ChangeData cd) {
throw new UnsupportedOperationException();
@@ -64,16 +81,31 @@
@Override
public ChangeDataSource getSource(Predicate<ChangeData> p)
throws QueryParseException {
- return new Source();
+ return new Source(p);
}
@Override
- public void finishIndex() {
+ public Schema<ChangeData> getSchema() {
+ return schema;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void markReady(boolean ready) {
throw new UnsupportedOperationException();
}
}
private static class Source implements ChangeDataSource {
+ private final Predicate<ChangeData> p;
+
+ Source(Predicate<ChangeData> p) {
+ this.p = p;
+ }
+
@Override
public int getCardinality() {
throw new UnsupportedOperationException();
@@ -88,15 +120,20 @@
public ResultSet<ChangeData> read() throws OrmException {
throw new UnsupportedOperationException();
}
+
+ @Override
+ public String toString() {
+ return p.toString();
+ }
}
- public static class QueryBuilder extends ChangeQueryBuilder {
+ public class QueryBuilder extends ChangeQueryBuilder {
QueryBuilder() {
super(
new QueryBuilder.Definition<ChangeData, QueryBuilder>(
QueryBuilder.class),
new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
- null, null, null, null, null, null),
+ null, null, null, null, null, indexes),
null);
}
@@ -110,7 +147,7 @@
return predicate("bar", value);
}
- private static Predicate<ChangeData> predicate(String name, String value) {
+ private Predicate<ChangeData> predicate(String name, String value) {
return new OperatorPredicate<ChangeData>(name, value) {
@Override
public boolean match(ChangeData object) throws OrmException {
@@ -126,20 +163,26 @@
}
private DummyIndex index;
+ private IndexCollection indexes;
private ChangeQueryBuilder queryBuilder;
- private IndexRewrite rewrite;
+ private IndexRewriteImpl rewrite;
@Override
public void setUp() throws Exception {
super.setUp();
- index = new DummyIndex();
+ index = new DummyIndex(V2);
+ indexes = new IndexCollection();
+ indexes.setSearchIndex(index);
queryBuilder = new QueryBuilder();
- rewrite = new IndexRewriteImpl(index);
+ rewrite = new IndexRewriteImpl(
+ indexes,
+ null,
+ new IndexRewriteImpl.BasicRewritesImpl(null));
}
public void testIndexPredicate() throws Exception {
Predicate<ChangeData> in = parse("file:a");
- assertEquals(wrap(in), rewrite(in));
+ assertEquals(query(in), rewrite(in));
}
public void testNonIndexPredicate() throws Exception {
@@ -149,33 +192,37 @@
public void testIndexPredicates() throws Exception {
Predicate<ChangeData> in = parse("file:a file:b");
- assertEquals(wrap(in), rewrite(in));
+ assertEquals(query(in), rewrite(in));
}
public void testNonIndexPredicates() throws Exception {
Predicate<ChangeData> in = parse("foo:a OR foo:b");
- assertSame(in, rewrite(in));
+ assertEquals(in, rewrite(in));
}
public void testOneIndexPredicate() throws Exception {
Predicate<ChangeData> in = parse("foo:a file:b");
Predicate<ChangeData> out = rewrite(in);
- assertSame(AndPredicate.class, out.getClass());
- assertEquals(ImmutableList.of(in.getChild(0), wrap(in.getChild(1))),
+ assertSame(AndSource.class, out.getClass());
+ assertEquals(
+ ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
out.getChildren());
}
public void testThreeLevelTreeWithAllIndexPredicates() throws Exception {
Predicate<ChangeData> in =
parse("-status:abandoned (status:open OR status:merged)");
- assertEquals(wrap(in), rewrite.rewrite(in));
+ assertEquals(
+ query(parse("status:new OR status:submitted OR status:draft OR status:merged")),
+ rewrite.rewrite(in));
}
public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception {
Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)");
Predicate<ChangeData> out = rewrite(in);
- assertEquals(AndPredicate.class, out.getClass());
- assertEquals(ImmutableList.of(in.getChild(0), wrap(in.getChild(1))),
+ assertEquals(AndSource.class, out.getClass());
+ assertEquals(
+ ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
out.getChildren());
}
@@ -183,20 +230,20 @@
Predicate<ChangeData> in =
parse("file:a OR foo:b OR file:c OR foo:d");
Predicate<ChangeData> out = rewrite(in);
- assertSame(OrPredicate.class, out.getClass());
+ assertSame(OrSource.class, out.getClass());
assertEquals(ImmutableList.of(
- in.getChild(1), in.getChild(3),
- wrap(Predicate.or(in.getChild(0), in.getChild(2)))),
+ query(Predicate.or(in.getChild(0), in.getChild(2))),
+ in.getChild(1), in.getChild(3)),
out.getChildren());
}
- public void testDuplicateSimpleNonIndexOnlyPredicates() throws Exception {
+ public void testIndexAndNonIndexPredicates() throws Exception {
Predicate<ChangeData> in = parse("status:new bar:p file:a");
Predicate<ChangeData> out = rewrite(in);
- assertSame(AndPredicate.class, out.getClass());
+ assertSame(AndSource.class, out.getClass());
assertEquals(ImmutableList.of(
- in.getChild(0), in.getChild(1),
- wrap(Predicate.and(in.getChild(0), in.getChild(2)))),
+ query(Predicate.and(in.getChild(0), in.getChild(2))),
+ in.getChild(1)),
out.getChildren());
}
@@ -204,10 +251,10 @@
Predicate<ChangeData> in =
parse("(status:new OR status:draft) bar:p file:a");
Predicate<ChangeData> out = rewrite(in);
- assertSame(AndPredicate.class, out.getClass());
+ assertSame(AndSource.class, out.getClass());
assertEquals(ImmutableList.of(
- in.getChild(0), in.getChild(1),
- wrap(Predicate.and(in.getChild(0), in.getChild(2)))),
+ query(Predicate.and(in.getChild(0), in.getChild(2))),
+ in.getChild(1)),
out.getChildren());
}
@@ -215,10 +262,10 @@
Predicate<ChangeData> in =
parse("(status:new OR file:a) bar:p file:b");
Predicate<ChangeData> out = rewrite(in);
- assertSame(AndPredicate.class, out.getClass());
+ assertSame(AndSource.class, out.getClass());
assertEquals(ImmutableList.of(
- in.getChild(1),
- wrap(Predicate.and(in.getChild(0), in.getChild(2)))),
+ query(Predicate.and(in.getChild(0), in.getChild(2))),
+ in.getChild(1)),
out.getChildren());
}
@@ -238,6 +285,19 @@
status("(is:new is:draft) OR (is:merged OR is:submitted)"));
}
+ public void testUnsupportedIndexOperator() throws Exception {
+ Predicate<ChangeData> in = parse("status:merged file:a");
+ assertEquals(query(in), rewrite(in));
+
+ indexes.setSearchIndex(new DummyIndex(V1));
+ Predicate<ChangeData> out = rewrite(in);
+ assertTrue(out instanceof AndPredicate);
+ assertEquals(ImmutableList.of(
+ query(in.getChild(0)),
+ in.getChild(1)),
+ out.getChildren());
+ }
+
private Predicate<ChangeData> parse(String query) throws QueryParseException {
return queryBuilder.parse(query);
}
@@ -246,9 +306,9 @@
return rewrite.rewrite(in);
}
- private PredicateWrapper wrap(Predicate<ChangeData> p)
+ private IndexedChangeQuery query(Predicate<ChangeData> p)
throws QueryParseException {
- return new PredicateWrapper(index, p);
+ return new IndexedChangeQuery(index, p);
}
private Set<Change.Status> status(String query) throws QueryParseException {
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
index 67c4d0e..7e32bac 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
@@ -17,7 +17,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeSchemas;
import com.google.inject.Inject;
import com.google.inject.ProvisionException;
@@ -31,8 +31,8 @@
class IndexVersionCheck implements LifecycleListener {
public static final Map<String, Integer> SCHEMA_VERSIONS = ImmutableMap.of(
- SolrChangeIndex.CHANGES_OPEN, ChangeField.SCHEMA_VERSION,
- SolrChangeIndex.CHANGES_CLOSED, ChangeField.SCHEMA_VERSION);
+ SolrChangeIndex.CHANGES_OPEN, ChangeSchemas.getLatest().getVersion(),
+ SolrChangeIndex.CHANGES_CLOSED, ChangeSchemas.getLatest().getVersion());
public static File solrIndexConfig(SitePaths sitePaths) {
return new File(sitePaths.index_dir, "gerrit_index.config");
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
index c28a0fc..b95978e 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
@@ -14,8 +14,8 @@
package com.google.gerrit.solr;
-import static com.google.gerrit.server.query.change.IndexRewriteImpl.CLOSED_STATUSES;
-import static com.google.gerrit.server.query.change.IndexRewriteImpl.OPEN_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
import static com.google.gerrit.solr.IndexVersionCheck.SCHEMA_VERSIONS;
import static com.google.gerrit.solr.IndexVersionCheck.solrIndexConfig;
@@ -34,15 +34,15 @@
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldDef.FillArgs;
import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexRewriteImpl;
+import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gerrit.server.query.change.IndexRewriteImpl;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
import org.apache.lucene.search.Query;
import org.apache.solr.client.solrj.SolrQuery;
@@ -52,7 +52,6 @@
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
@@ -66,7 +65,6 @@
import java.util.Set;
/** Secondary index implementation using a remote Solr instance. */
-@Singleton
class SolrChangeIndex implements ChangeIndex, LifecycleListener {
public static final String CHANGES_OPEN = "changes_open";
public static final String CHANGES_CLOSED = "changes_closed";
@@ -74,32 +72,40 @@
private final FillArgs fillArgs;
private final SitePaths sitePaths;
+ private final IndexCollection indexes;
private final CloudSolrServer openIndex;
private final CloudSolrServer closedIndex;
+ private final Schema<ChangeData> schema;
- @Inject
SolrChangeIndex(
@GerritServerConfig Config cfg,
FillArgs fillArgs,
- SitePaths sitePaths) throws IOException {
+ SitePaths sitePaths,
+ IndexCollection indexes,
+ Schema<ChangeData> schema,
+ String base) throws IOException {
this.fillArgs = fillArgs;
this.sitePaths = sitePaths;
+ this.indexes = indexes;
+ this.schema = schema;
String url = cfg.getString("index", "solr", "url");
if (Strings.isNullOrEmpty(url)) {
throw new IllegalStateException("index.solr.url must be supplied");
}
+ base = Strings.nullToEmpty(base);
openIndex = new CloudSolrServer(url);
- openIndex.setDefaultCollection(CHANGES_OPEN);
+ openIndex.setDefaultCollection(base + CHANGES_OPEN);
closedIndex = new CloudSolrServer(url);
- closedIndex.setDefaultCollection(CHANGES_CLOSED);
+ closedIndex.setDefaultCollection(base + CHANGES_CLOSED);
}
@Override
public void start() {
- // Do nothing.
+ indexes.setSearchIndex(this);
+ indexes.addWriteIndex(this);
}
@Override
@@ -109,6 +115,16 @@
}
@Override
+ public Schema<ChangeData> getSchema() {
+ return schema;
+ }
+
+ @Override
+ public void close() {
+ stop();
+ }
+
+ @Override
public ListenableFuture<Void> insert(ChangeData cd) throws IOException {
String id = cd.getId().toString();
SolrInputDocument doc = toDocument(cd);
@@ -225,6 +241,11 @@
}
@Override
+ public String toString() {
+ return query.getQuery();
+ }
+
+ @Override
public ResultSet<ChangeData> read() throws OrmException {
try {
// TODO Sort documents during merge to select only top N.
@@ -265,7 +286,7 @@
private SolrInputDocument toDocument(ChangeData cd) throws IOException {
try {
SolrInputDocument result = new SolrInputDocument();
- for (FieldDef<ChangeData, ?> f : ChangeField.ALL.values()) {
+ for (FieldDef<ChangeData, ?> f : schema.getFields().values()) {
if (f.isRepeatable()) {
add(result, f, (Iterable<?>) f.get(cd, fillArgs));
} else {
@@ -304,14 +325,14 @@
}
@Override
- public void finishIndex() throws IOException,
- ConfigInvalidException {
+ public void markReady(boolean ready) throws IOException {
// TODO Move the schema version information to a special meta-document
FileBasedConfig cfg = new FileBasedConfig(
solrIndexConfig(sitePaths),
FS.detect());
for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
- cfg.setInt("index", e.getKey(), "schemaVersion", e.getValue());
+ cfg.setInt("index", e.getKey(), "schemaVersion",
+ ready ? e.getValue() : -1);
}
cfg.save();
}
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
index 4e1a548..8c614f7 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
@@ -15,20 +15,33 @@
package com.google.gerrit.solr;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.index.IndexModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
public class SolrIndexModule extends LifecycleModule {
private final boolean checkVersion;
private final int threads;
+ private final String base;
public SolrIndexModule() {
- this(true, 0);
+ this(true, 0, null);
}
- public SolrIndexModule(boolean checkVersion, int threads) {
+ public SolrIndexModule(boolean checkVersion, int threads, String base) {
this.checkVersion = checkVersion;
this.threads = threads;
+ this.base = base;
}
@Override
@@ -40,4 +53,14 @@
listener().to(IndexVersionCheck.class);
}
}
+
+ @Provides
+ @Singleton
+ public SolrChangeIndex getChangeIndex(@GerritServerConfig Config cfg,
+ SitePaths sitePaths,
+ IndexCollection indexes,
+ FillArgs fillArgs) throws IOException {
+ return new SolrChangeIndex(cfg, fillArgs, sitePaths, indexes,
+ ChangeSchemas.getLatest(), base);
+ }
}
diff --git a/plugins/replication b/plugins/replication
index 5353cee..50972e3 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 5353ceeb979b166e641db8e9fb356eaadd6b50fd
+Subproject commit 50972e33baba9eaa034ef9637efaf27e928d7a46
diff --git a/website/releases/index.html b/website/releases/index.html
index 735bf1e..3faf054 100644
--- a/website/releases/index.html
+++ b/website/releases/index.html
@@ -45,6 +45,7 @@
var frg = doc.createDocumentFragment();
var rx = /^gerrit(?:-full)?-([0-9.]+(?:-rc[0-9]+)?)[.]war/;
var rel = 'http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/';
+ var src = 'https://gerrit.googlesource.com/gerrit/+/'
data.items.sort(function(a,b) {
var av = rx.exec(a.name);
@@ -101,6 +102,16 @@
tr.appendChild(td);
td = doc.createElement('td');
+ td.className = 'size';
+ if (f.size/(1024*1024) < 1) {
+ sizeText = Math.round(f.size/1024*10)/10 + ' KiB';
+ } else {
+ sizeText = Math.round(f.size/(1024*1024)*10)/10 + ' MiB';
+ }
+ td.appendChild(doc.createTextNode(sizeText));
+ tr.appendChild(td);
+
+ td = doc.createElement('td');
if (v && f.name.indexOf('-rc') < 0) {
a = doc.createElement('a');
a.href = rel + 'ReleaseNotes-' + v[1] + '.html';
@@ -110,13 +121,12 @@
tr.appendChild(td);
td = doc.createElement('td');
- td.className = 'size';
- if (f.size/(1024*1024) < 1) {
- sizeText = Math.round(f.size/1024*10)/10 + ' KiB';
- } else {
- sizeText = Math.round(f.size/(1024*1024)*10)/10 + ' MiB';
+ if (v) {
+ a = doc.createElement('a');
+ a.href = src + 'v' + v[1];
+ a.appendChild(doc.createTextNode('src'));
+ td.appendChild(a);
}
- td.appendChild(doc.createTextNode(sizeText));
tr.appendChild(td);
frg.appendChild(tr);
@@ -128,12 +138,12 @@
tr.appendChild(th);
th = doc.createElement('th');
- tr.appendChild(th);
-
- th = doc.createElement('th');
th.appendChild(doc.createTextNode('Size'));
tr.appendChild(th);
+ tr.appendChild(doc.createElement('th'));
+ tr.appendChild(doc.createElement('th'));
+
var table = doc.createElement('table');
table.appendChild(tr);
table.appendChild(frg);