Allow watching specific branches or any other search query

Any valid search string is now a valid filter expression or a
watched project.  The only operator not supported here is the
is:watched operator, because that creates a recursive call that
would never succeed.

The change turned out far bigger than it should be due to the request
scope requirement for the query builder.  We had to rearrange a lot
of code to ensure we always have the request scope available in order
to construct a query builder and execute the filter expressions.

Bug: issue 492
Change-Id: I199d9b215e000c049279cd8e86e7a36386fee0fb
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index b4fc55d..e219074 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -48,7 +48,7 @@
   void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
 
   @SignInRequired
-  void addProjectWatch(String projectName,
+  void addProjectWatch(String projectName, String filter,
       AsyncCallback<AccountProjectWatchInfo> callback);
 
   @SignInRequired
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 40fe731..c556770 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -32,6 +32,7 @@
   String removeReviewer();
   String removeReviewerCell();
   String addSshKeyPanel();
+  String addWatchPanel();
   String approvalCategoryList();
   String approvalTable();
   String approvalhint();
@@ -180,4 +181,5 @@
   String useridentity();
   String usernameField();
   String version();
+  String watchedProjectFilter();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index e161c14..3756bed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -80,6 +80,9 @@
 
   String buttonWatchProject();
   String defaultProjectName();
+  String defaultFilter();
+  String watchedProjectName();
+  String watchedProjectFilter();
   String watchedProjectColumnEmailNotifications();
   String watchedProjectColumnNewChanges();
   String watchedProjectColumnAllComments();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index b74add4..29ee14a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -61,6 +61,9 @@
 
 buttonWatchProject = Watch
 defaultProjectName = Project Name
+defaultFilter = branch:name, or other search expression
+watchedProjectName = Project Name
+watchedProjectFilter = Only If
 watchedProjectColumnEmailNotifications = Email Notifications
 watchedProjectColumnNewChanges = New Changes
 watchedProjectColumnAllComments = All Comments
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index eb8a8f1..28099bf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -38,8 +38,11 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtjsonrpc.client.VoidResult;
@@ -51,7 +54,9 @@
   private WatchTable watches;
 
   private Button addNew;
+  private NpTextBox nameBox;
   private SuggestBox nameTxt;
+  private NpTextBox filterTxt;
   private Button delSel;
   private boolean submitOnSelection;
 
@@ -60,32 +65,30 @@
     super.onInitUI();
 
     {
-      final FlowPanel fp = new FlowPanel();
-
-      final NpTextBox box = new NpTextBox();
-      nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), box);
-      box.setVisibleLength(50);
-      box.setText(Util.C.defaultProjectName());
-      box.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
-      box.addFocusHandler(new FocusHandler() {
+      nameBox = new NpTextBox();
+      nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), nameBox);
+      nameBox.setVisibleLength(50);
+      nameBox.setText(Util.C.defaultProjectName());
+      nameBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
+      nameBox.addFocusHandler(new FocusHandler() {
         @Override
         public void onFocus(FocusEvent event) {
-          if (Util.C.defaultProjectName().equals(box.getText())) {
-            box.setText("");
-            box.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
+          if (Util.C.defaultProjectName().equals(nameBox.getText())) {
+            nameBox.setText("");
+            nameBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
           }
         }
       });
-      box.addBlurHandler(new BlurHandler() {
+      nameBox.addBlurHandler(new BlurHandler() {
         @Override
         public void onBlur(BlurEvent event) {
-          if ("".equals(box.getText())) {
-            box.setText(Util.C.defaultProjectName());
-            box.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
+          if ("".equals(nameBox.getText())) {
+            nameBox.setText(Util.C.defaultProjectName());
+            nameBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
           }
         }
       });
-      box.addKeyPressHandler(new KeyPressHandler() {
+      nameBox.addKeyPressHandler(new KeyPressHandler() {
         @Override
         public void onKeyPress(KeyPressEvent event) {
           submitOnSelection = false;
@@ -108,7 +111,37 @@
           }
         }
       });
-      fp.add(nameTxt);
+
+      filterTxt = new NpTextBox();
+      filterTxt.setVisibleLength(50);
+      filterTxt.setText(Util.C.defaultFilter());
+      filterTxt.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
+      filterTxt.addFocusHandler(new FocusHandler() {
+        @Override
+        public void onFocus(FocusEvent event) {
+          if (Util.C.defaultFilter().equals(filterTxt.getText())) {
+            filterTxt.setText("");
+            filterTxt.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
+          }
+        }
+      });
+      filterTxt.addBlurHandler(new BlurHandler() {
+        @Override
+        public void onBlur(BlurEvent event) {
+          if ("".equals(filterTxt.getText())) {
+            filterTxt.setText(Util.C.defaultFilter());
+            filterTxt.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
+          }
+        }
+      });
+      filterTxt.addKeyPressHandler(new KeyPressHandler() {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+            doAddNew();
+          }
+        }
+      });
 
       addNew = new Button(Util.C.buttonWatchProject());
       addNew.addClickHandler(new ClickHandler() {
@@ -117,24 +150,40 @@
           doAddNew();
         }
       });
+
+      final Grid grid = new Grid(2, 2);
+      grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
+      grid.setText(0, 0, Util.C.watchedProjectName());
+      grid.setWidget(0, 1, nameTxt);
+
+      grid.setText(1, 0, Util.C.watchedProjectFilter());
+      grid.setWidget(1, 1, filterTxt);
+
+      final CellFormatter fmt = grid.getCellFormatter();
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().header());
+      fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().header());
+      fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
+
+      final FlowPanel fp = new FlowPanel();
+      fp.setStyleName(Gerrit.RESOURCES.css().addWatchPanel());
+      fp.add(grid);
       fp.add(addNew);
       add(fp);
     }
 
     watches = new WatchTable();
     add(watches);
-    {
-      final FlowPanel fp = new FlowPanel();
-      delSel = new Button(Util.C.buttonDeleteSshKey());
-      delSel.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          watches.deleteChecked();
-        }
-      });
-      fp.add(delSel);
-      add(fp);
-    }
+
+    delSel = new Button(Util.C.buttonDeleteSshKey());
+    delSel.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        watches.deleteChecked();
+      }
+    });
+    add(delSel);
   }
 
   void doAddNew() {
@@ -144,11 +193,23 @@
       return;
     }
 
+    String filter = filterTxt.getText();
+    if (filter == null || filter.isEmpty()
+        || filter.equals(Util.C.defaultFilter())) {
+      filter = null;
+    }
+
     addNew.setEnabled(false);
-    Util.ACCOUNT_SVC.addProjectWatch(projectName,
+    nameBox.setEnabled(false);
+    filterTxt.setEnabled(false);
+
+    Util.ACCOUNT_SVC.addProjectWatch(projectName, filter,
         new GerritCallback<AccountProjectWatchInfo>() {
           public void onSuccess(final AccountProjectWatchInfo result) {
             addNew.setEnabled(true);
+            nameBox.setEnabled(true);
+            filterTxt.setEnabled(true);
+
             nameTxt.setText("");
             watches.insertWatch(result);
           }
@@ -156,6 +217,9 @@
           @Override
           public void onFailure(final Throwable caught) {
             addNew.setEnabled(true);
+            nameBox.setEnabled(true);
+            filterTxt.setEnabled(true);
+
             super.onFailure(caught);
           }
         });
@@ -177,8 +241,7 @@
     WatchTable() {
       table.setWidth("");
       table.insertRow(1);
-      table.setText(0, 2, com.google.gerrit.client.changes.Util.C
-          .changeTableColumnProject());
+      table.setText(0, 2, Util.C.watchedProjectName());
       table.setText(0, 3, Util.C.watchedProjectColumnEmailNotifications());
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
@@ -253,8 +316,16 @@
     }
 
     void populate(final int row, final AccountProjectWatchInfo k) {
+      final FlowPanel fp = new FlowPanel();
+      fp.add(new ProjectLink(k.getProject().getNameKey(), Status.NEW));
+      if (k.getWatch().getFilter() != null) {
+        Label filter = new Label(k.getWatch().getFilter());
+        filter.setStyleName(Gerrit.RESOURCES.css().watchedProjectFilter());
+        fp.add(filter);
+      }
+
       table.setWidget(row, 1, new CheckBox());
-      table.setWidget(row, 2, new ProjectLink(k.getProject().getNameKey(), Status.NEW));
+      table.setWidget(row, 2, fp);
       {
         final CheckBox notifyNewChanges = new CheckBox();
         notifyNewChanges.addClickHandler(new ClickHandler() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 2b2fbcc..d44aa78 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -1029,6 +1029,15 @@
   font-weight: bold;
 }
 
+.addWatchPanel {
+  margin-top: 10px;
+  padding: 5px 5px 5px 5px;
+}
+.watchedProjectFilter {
+  margin-left: 1em;
+  color: grey;
+}
+
 .addSshKeyPanel {
   margin-top: 10px;
   background-color: trimColor;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
index c01d65e..332e262 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
@@ -89,7 +89,7 @@
   private final ChangeControl.Factory changeControlFactory;
   private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
 
-  private final Provider<ChangeQueryBuilder> queryBuilder;
+  private final ChangeQueryBuilder.Factory queryBuilder;
   private final Provider<ChangeQueryRewriter> queryRewriter;
 
   @Inject
@@ -97,7 +97,7 @@
       final Provider<CurrentUser> currentUser,
       final ChangeControl.Factory changeControlFactory,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
-      final Provider<ChangeQueryBuilder> queryBuilder,
+      final ChangeQueryBuilder.Factory queryBuilder,
       final Provider<ChangeQueryRewriter> queryRewriter) {
     super(schema, currentUser);
     this.currentUser = currentUser;
@@ -144,10 +144,8 @@
       final int limit, final String key, final Comparator<Change> cmp)
       throws OrmException, InvalidQueryException {
     try {
-      final ChangeQueryBuilder builder = queryBuilder.get();
-      final Predicate<ChangeData> visibleToMe =
-          builder.visibleto(currentUser.get());
-
+      final ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
+      final Predicate<ChangeData> visibleToMe = builder.is_visible();
       Predicate<ChangeData> q = builder.parse(query);
       q = Predicate.and(q, //
           cmp == QUERY_PREV //
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index aa2e95b..dd1866c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.data.AccountProjectWatchInfo;
 import com.google.gerrit.common.data.AccountService;
 import com.google.gerrit.common.data.AgreementInfo;
+import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.reviewdb.Account;
@@ -29,8 +30,11 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtorm.client.OrmDuplicateKeyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,18 +51,21 @@
   private final AccountCache accountCache;
   private final ProjectControl.Factory projectControlFactory;
   private final AgreementInfoFactory.Factory agreementInfoFactory;
+  private final ChangeQueryBuilder.Factory queryBuilder;
 
   @Inject
   AccountServiceImpl(final Provider<ReviewDb> schema,
       final Provider<IdentifiedUser> identifiedUser,
       final AccountCache accountCache,
       final ProjectControl.Factory projectControlFactory,
-      final AgreementInfoFactory.Factory agreementInfoFactory) {
+      final AgreementInfoFactory.Factory agreementInfoFactory,
+      final ChangeQueryBuilder.Factory queryBuilder) {
     super(schema, identifiedUser);
     this.currentUser = identifiedUser;
     this.accountCache = accountCache;
     this.projectControlFactory = projectControlFactory;
     this.agreementInfoFactory = agreementInfoFactory;
+    this.queryBuilder = queryBuilder;
   }
 
   public void myAccount(final AsyncCallback<Account> callback) {
@@ -137,19 +144,31 @@
     });
   }
 
-  public void addProjectWatch(final String projectName,
+  public void addProjectWatch(final String projectName, final String filter,
       final AsyncCallback<AccountProjectWatchInfo> callback) {
     run(callback, new Action<AccountProjectWatchInfo>() {
       public AccountProjectWatchInfo run(ReviewDb db) throws OrmException,
-          NoSuchProjectException {
+          NoSuchProjectException, InvalidQueryException {
         final Project.NameKey nameKey = new Project.NameKey(projectName);
         final ProjectControl ctl = projectControlFactory.validateFor(nameKey);
 
-        final AccountProjectWatch watch =
-            new AccountProjectWatch(
-                new AccountProjectWatch.Key(((IdentifiedUser) ctl
-                    .getCurrentUser()).getAccountId(), nameKey));
-        db.accountProjectWatches().insert(Collections.singleton(watch));
+        if (filter != null) {
+          try {
+            queryBuilder.create(currentUser.get()).parse(filter);
+          } catch (QueryParseException badFilter) {
+            throw new InvalidQueryException(badFilter.getMessage(), filter);
+          }
+        }
+
+        AccountProjectWatch watch =
+            new AccountProjectWatch(new AccountProjectWatch.Key(
+                ((IdentifiedUser) ctl.getCurrentUser()).getAccountId(),
+                nameKey, filter));
+        try {
+          db.accountProjectWatches().insert(Collections.singleton(watch));
+        } catch (OrmDuplicateKeyException alreadyHave) {
+          watch = db.accountProjectWatches().get(watch.getKey());
+        }
         return new AccountProjectWatchInfo(watch, ctl.getProject());
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
index 5f0851e..4a4d9d1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
@@ -127,7 +127,6 @@
       // Email the reviewers
       final AbandonedSender cm = abandonedSenderFactory.create(change);
       cm.setFrom(currentUser.getAccountId());
-      cm.setReviewDb(db);
       cm.setChangeMessage(cmsg);
       cm.send();
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
index 8e64469..4ab9072 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.CanSubmitResult;
@@ -44,8 +45,8 @@
   private final FunctionState.Factory functionState;
   private final IdentifiedUser user;
   private final ChangeDetailFactory.Factory changeDetailFactory;
-  @Inject
-  private ChangeControl.Factory changeControlFactory;
+  private final ChangeControl.Factory changeControlFactory;
+  private final MergeOp.Factory opFactory;
 
   private final PatchSet.Id patchSetId;
 
@@ -53,13 +54,17 @@
   SubmitAction(final ReviewDb db, final MergeQueue mq, final ApprovalTypes at,
       final FunctionState.Factory fs, final IdentifiedUser user,
       final ChangeDetailFactory.Factory changeDetailFactory,
+      final ChangeControl.Factory changeControlFactory,
+      final MergeOp.Factory opFactory,
       @Assisted final PatchSet.Id patchSetId) {
     this.db = db;
     this.merger = mq;
     this.approvalTypes = at;
     this.functionState = fs;
     this.user = user;
+    this.changeControlFactory = changeControlFactory;
     this.changeDetailFactory = changeDetailFactory;
+    this.opFactory = opFactory;
 
     this.patchSetId = patchSetId;
   }
@@ -76,7 +81,7 @@
     CanSubmitResult err =
         changeControl.canSubmit(patchSetId, db, approvalTypes, functionState);
     if (err == CanSubmitResult.OK) {
-      ChangeUtil.submit(patchSetId, user, db, merger);
+      ChangeUtil.submit(opFactory, patchSetId, user, db, merger);
       return changeDetailFactory.create(changeId).call();
     } else {
       throw new IllegalStateException(err.getMessage());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java
index d04f4ef..ae36388 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java
@@ -130,7 +130,6 @@
     final AddReviewerSender cm;
     cm = addReviewerSenderFactory.create(control.getChange());
     cm.setFrom(currentUser.getAccountId());
-    cm.setReviewDb(db);
     cm.addReviewers(added);
     cm.send();
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
index 5d1565b..52bef2b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
@@ -16,9 +16,12 @@
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
+import com.google.gwtorm.client.StringKey;
 
 /** An {@link Account} interested in a {@link Project}. */
 public final class AccountProjectWatch {
+  public static final String FILTER_ALL = "*";
+
   public static class Key extends CompoundKey<Account.Id> {
     private static final long serialVersionUID = 1L;
 
@@ -28,14 +31,19 @@
     @Column(id = 2)
     protected Project.NameKey projectName;
 
+    @Column(id = 3)
+    protected Filter filter;
+
     protected Key() {
       accountId = new Account.Id();
       projectName = new Project.NameKey();
+      filter = new Filter();
     }
 
-    public Key(final Account.Id a, final Project.NameKey g) {
+    public Key(Account.Id a, Project.NameKey g, String f) {
       accountId = a;
       projectName = g;
+      filter = new Filter(f);
     }
 
     @Override
@@ -45,7 +53,31 @@
 
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {projectName};
+      return new com.google.gwtorm.client.Key<?>[] {projectName, filter};
+    }
+  }
+
+  public static class Filter extends StringKey<com.google.gwtorm.client.Key<?>> {
+    private static final long serialVersionUID = 1L;
+
+    @Column(id = 1)
+    protected String filter;
+
+    protected Filter() {
+    }
+
+    public Filter(String f) {
+      filter = f != null && !f.isEmpty() ? f : FILTER_ALL;
+    }
+
+    @Override
+    public String get() {
+      return filter;
+    }
+
+    @Override
+    protected void set(String newValue) {
+      filter = newValue;
     }
   }
 
@@ -83,6 +115,10 @@
     return key.projectName;
   }
 
+  public String getFilter() {
+    return FILTER_ALL.equals(key.filter.get()) ? null : key.filter.get();
+  }
+
   public boolean isNotifyNewChanges() {
     return notifyNewChanges;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
index ff6f43d..1bd2066 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
@@ -15,12 +15,13 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project.NameKey;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -43,7 +44,7 @@
   }
 
   @Override
-  public Set<NameKey> getWatchedProjects() {
+  public Collection<AccountProjectWatch> getNotificationFilters() {
     return Collections.emptySet();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 7f562c9..36fc2ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.TrackingId;
 import com.google.gerrit.server.config.TrackingFooter;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gwtorm.client.AtomicUpdate;
 import com.google.gwtorm.client.OrmConcurrencyException;
@@ -135,8 +136,8 @@
     db.trackingIds().delete(toDelete);
   }
 
-  public static void submit(PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db, MergeQueue merger)
-      throws OrmException {
+  public static void submit(MergeOp.Factory opFactory, PatchSet.Id patchSetId,
+      IdentifiedUser user, ReviewDb db, MergeQueue merger) throws OrmException {
     final Change.Id changeId = patchSetId.getParentKey();
     final PatchSetApproval approval = createSubmitApproval(patchSetId, user, db);
 
@@ -154,7 +155,7 @@
     });
 
     if (change.getStatus() == Change.Status.SUBMITTED) {
-      merger.merge(change.getDest());
+      merger.merge(opFactory, change.getDest());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 207d5cc..751d215 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.servlet.RequestScoped;
 
+import java.util.Collection;
 import java.util.Set;
 
 /**
@@ -60,8 +61,8 @@
   /** Set of changes starred by this user. */
   public abstract Set<Change.Id> getStarredChanges();
 
-  /** Set of project that are watched by this user */
-  public abstract Set<Project.NameKey> getWatchedProjects();
+  /** Filters selecting changes the user wants to monitor. */
+  public abstract Collection<AccountProjectWatch> getNotificationFilters();
 
   /** Is the user a non-interactive user? */
   public boolean isBatchUser() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index a5c780e..78cbed3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.StarredChange;
 import com.google.gerrit.server.account.AccountCache;
@@ -43,9 +42,11 @@
 import java.net.MalformedURLException;
 import java.net.SocketAddress;
 import java.net.URL;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.TimeZone;
 
@@ -145,7 +146,7 @@
   private Set<String> emailAddresses;
   private Set<AccountGroup.Id> effectiveGroups;
   private Set<Change.Id> starredChanges;
-  private Set<Project.NameKey> watchedProjects;
+  private Collection<AccountProjectWatch> notificationFilters;
 
   private IdentifiedUser(final AccessPath accessPath,
       final AuthConfig authConfig, final Provider<String> canonicalUrl,
@@ -237,24 +238,22 @@
   }
 
   @Override
-  public Set<Project.NameKey> getWatchedProjects() {
-    if (watchedProjects == null) {
+  public Collection<AccountProjectWatch> getNotificationFilters() {
+    if (notificationFilters == null) {
       if (dbProvider == null) {
         throw new OutOfScopeException("Not in request scoped user");
       }
-      final Set<Project.NameKey> h = new HashSet<Project.NameKey>();
+      List<AccountProjectWatch> r;
       try {
-        for (AccountProjectWatch projectWatch : dbProvider.get()
-            .accountProjectWatches().byAccount(getAccountId())) {
-          h.add(projectWatch.getProjectNameKey());
-        }
+        r = dbProvider.get().accountProjectWatches() //
+            .byAccount(getAccountId()).toList();
       } catch (OrmException e) {
-        log.warn("Cannot query project watches of a user", e);
+        log.warn("Cannot query notification filters of a user", e);
+        r = Collections.emptyList();
       }
-      watchedProjects = Collections.unmodifiableSet(h);
+      notificationFilters = Collections.unmodifiableList(r);
     }
-
-    return watchedProjects;
+    return notificationFilters;
   }
 
   public PersonIdent newRefLogIdent() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index d44643b..e422b19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Project.NameKey;
 import com.google.gerrit.server.config.AuthConfig;
@@ -22,6 +23,7 @@
 import com.google.inject.assistedinject.Assisted;
 
 import java.net.SocketAddress;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -59,7 +61,7 @@
   }
 
   @Override
-  public Set<NameKey> getWatchedProjects() {
+  public Collection<AccountProjectWatch> getNotificationFilters() {
     return Collections.emptySet();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
index b21df8a..3293324 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Project.NameKey;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -73,7 +75,7 @@
   }
 
   @Override
-  public Set<NameKey> getWatchedProjects() {
+  public Collection<AccountProjectWatch> getNotificationFilters() {
     return Collections.emptySet();
   }
 
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 697f045..38a9272 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
@@ -132,7 +132,6 @@
     factory(PushAllProjectsOp.Factory.class);
 
     bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
-    factory(MergeOp.Factory.class);
     factory(ReloadSubmitQueueOp.Factory.class);
 
     bind(FromAddressGenerator.class).toProvider(
@@ -142,12 +141,6 @@
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
     factory(FunctionState.Factory.class);
-
-    factory(AbandonedSender.Factory.class);
-    factory(CommentSender.Factory.class);
-    factory(MergedSender.Factory.class);
-    factory(MergeFailSender.Factory.class);
-    factory(RegisterNewEmailSender.Factory.class);
     factory(ReplicationUser.Factory.class);
 
     install(new LifecycleModule() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 1125b05..5a75995 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -21,9 +21,15 @@
 import com.google.gerrit.server.RequestCleanup;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.AddReviewerSender;
+import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.MergeFailSender;
+import com.google.gerrit.server.mail.MergedSender;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.project.ChangeControl;
@@ -41,14 +47,15 @@
         RequestScoped.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
     bind(AccountResolver.class);
-    bind(ChangeQueryBuilder.class);
     bind(ChangeQueryRewriter.class);
 
     bind(ChangeControl.Factory.class).in(SINGLETON);
     bind(GroupControl.Factory.class).in(SINGLETON);
     bind(ProjectControl.Factory.class).in(SINGLETON);
 
+    factory(ChangeQueryBuilder.Factory.class);
     factory(ReceiveCommits.Factory.class);
+    factory(MergeOp.Factory.class);
 
     // Not really per-request, but dammit, I don't know where else to
     // easily park this stuff.
@@ -57,5 +64,10 @@
     factory(CreateChangeSender.Factory.class);
     factory(PublishComments.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
+    factory(AbandonedSender.Factory.class);
+    factory(CommentSender.Factory.class);
+    factory(MergedSender.Factory.class);
+    factory(MergeFailSender.Factory.class);
+    factory(RegisterNewEmailSender.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
index d6c8f4a..a7a969f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
@@ -19,12 +19,30 @@
 
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.RemotePeer;
+import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+import com.google.inject.servlet.RequestScoped;
+
+import com.jcraft.jsch.HostKey;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.net.SocketAddress;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -38,18 +56,47 @@
       new HashMap<Branch.NameKey, RecheckJob>();
 
   private final WorkQueue workQueue;
-  private final MergeOp.Factory opFactory;
+  private final Provider<MergeOp.Factory> bgFactory;
 
   @Inject
-  ChangeMergeQueue(final WorkQueue wq, final MergeOp.Factory of) {
+  ChangeMergeQueue(final WorkQueue wq, Injector parent) {
     workQueue = wq;
-    opFactory = of;
+
+    Injector child = parent.createChildInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bindScope(RequestScoped.class, MyScope.REQUEST);
+        install(new GerritRequestModule());
+
+        bind(CurrentUser.class).to(IdentifiedUser.class);
+        bind(IdentifiedUser.class).toProvider(new Provider<IdentifiedUser>() {
+          @Override
+          public IdentifiedUser get() {
+            throw new OutOfScopeException("No user on merge thread");
+          }
+        });
+        bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
+            new Provider<SocketAddress>() {
+              @Override
+              public SocketAddress get() {
+                throw new OutOfScopeException("No remote peer on merge thread");
+              }
+            });
+        bind(SshInfo.class).toInstance(new SshInfo() {
+          @Override
+          public List<HostKey> getHostKeys() {
+            return Collections.emptyList();
+          }
+        });
+      }
+    });
+    bgFactory = child.getProvider(MergeOp.Factory.class);
   }
 
   @Override
-  public void merge(final Branch.NameKey branch) {
+  public void merge(MergeOp.Factory mof, Branch.NameKey branch) {
     if (start(branch)) {
-      mergeImpl(branch);
+      mergeImpl(mof, branch);
     }
   }
 
@@ -127,7 +174,7 @@
     e.needMerge = false;
   }
 
-  private void mergeImpl(final Branch.NameKey branch) {
+  private void mergeImpl(MergeOp.Factory opFactory, Branch.NameKey branch) {
     try {
       opFactory.create(branch).merge();
     } catch (Throwable e) {
@@ -137,6 +184,26 @@
     }
   }
 
+  private void mergeImpl(Branch.NameKey branch) {
+    try {
+      MyScope ctx = new MyScope();
+      MyScope old = MyScope.set(ctx);
+      try {
+        try {
+          bgFactory.get().create(branch).merge();
+        } finally {
+          ctx.cleanup.run();
+        }
+      } finally {
+        MyScope.set(old);
+      }
+    } catch (Throwable e) {
+      log.error("Merge attempt for " + branch + " failed", e);
+    } finally {
+      finish(branch);
+    }
+  }
+
   private synchronized void recheck(final RecheckJob e) {
     final long remainingDelay = e.recheckAt - System.currentTimeMillis();
     if (MILLISECONDS.convert(10, SECONDS) < remainingDelay) {
@@ -194,4 +261,65 @@
       return "recheck " + project.get() + " " + dest.getShortName();
     }
   }
+
+  private static class MyScope {
+    private static final ThreadLocal<MyScope> current =
+        new ThreadLocal<MyScope>();
+
+    private static MyScope getContext() {
+      final MyScope ctx = current.get();
+      if (ctx == null) {
+        throw new OutOfScopeException("Not in command/request");
+      }
+      return ctx;
+    }
+
+    static MyScope set(MyScope ctx) {
+      MyScope old = current.get();
+      current.set(ctx);
+      return old;
+    }
+
+    static final Scope REQUEST = new Scope() {
+      public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
+        return new Provider<T>() {
+          public T get() {
+            return getContext().get(key, creator);
+          }
+
+          @Override
+          public String toString() {
+            return String.format("%s[%s]", creator, REQUEST);
+          }
+        };
+      }
+
+      @Override
+      public String toString() {
+        return "MergeQueue.REQUEST";
+      }
+    };
+
+    private static final Key<RequestCleanup> RC_KEY =
+        Key.get(RequestCleanup.class);
+
+    private final RequestCleanup cleanup;
+    private final Map<Key<?>, Object> map;
+
+    MyScope() {
+      cleanup = new RequestCleanup();
+      map = new HashMap<Key<?>, Object>();
+      map.put(RC_KEY, cleanup);
+    }
+
+    synchronized <T> T get(Key<T> key, Provider<T> creator) {
+      @SuppressWarnings("unchecked")
+      T t = (T) map.get(key);
+      if (t == null) {
+        t = creator.get();
+        map.put(key, t);
+      }
+      return t;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 5280e0c..66e8d61 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -1179,7 +1179,6 @@
       if (submitter != null) {
         cm.setFrom(submitter.getAccountId());
       }
-      cm.setReviewDb(schema);
       cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
       cm.send();
     } catch (OrmException e) {
@@ -1241,7 +1240,6 @@
           cm.setFrom(submitter.getAccountId());
         }
       }
-      cm.setReviewDb(schema);
       cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
       cm.setChangeMessage(msg);
       cm.send();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
index abb0ab8..39017eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
@@ -19,7 +19,7 @@
 import java.util.concurrent.TimeUnit;
 
 public interface MergeQueue {
-  void merge(Branch.NameKey branch);
+  void merge(MergeOp.Factory mof, Branch.NameKey branch);
 
   void schedule(Branch.NameKey branch);
 
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 be357cf..fbcdc09 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
@@ -931,7 +931,6 @@
       cm = createChangeSenderFactory.create(change);
       cm.setFrom(me);
       cm.setPatchSet(ps, info);
-      cm.setReviewDb(db);
       cm.addReviewers(reviewers);
       cm.addExtraCC(cc);
       cm.send();
@@ -1234,7 +1233,6 @@
       cm.setFrom(me);
       cm.setPatchSet(ps, result.info);
       cm.setChangeMessage(result.msg);
-      cm.setReviewDb(db);
       cm.addReviewers(reviewers);
       cm.addExtraCC(cc);
       cm.addReviewers(oldReviewers);
@@ -1583,7 +1581,6 @@
       try {
         final MergedSender cm = mergedSenderFactory.create(result.change);
         cm.setFrom(currentUser.getAccountId());
-        cm.setReviewDb(db);
         cm.setPatchSet(result.patchSet, result.info);
         cm.setDest(new Branch.NameKey(project.getNameKey(),
             result.mergedIntoRef));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index bba69c8..99f8028 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -25,8 +25,8 @@
   }
 
   @Inject
-  public AbandonedSender(@Assisted Change c) {
-    super(c, "abandon");
+  public AbandonedSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "abandon");
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
index c9a6d98..be62ba0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.server.ssh.SshInfo;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -25,8 +26,9 @@
   }
 
   @Inject
-  public AddReviewerSender(@Assisted Change c) {
-    super(c);
+  public AddReviewerSender(EmailArguments ea, SshInfo sshInfo,
+      @Assisted Change c) {
+    super(ea, sshInfo, c);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index a010ac1..358c1cc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -38,8 +38,8 @@
   private List<PatchLineComment> inlineComments = Collections.emptyList();
 
   @Inject
-  public CommentSender(@Assisted Change c) {
-    super(c, "comment");
+  public CommentSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "comment");
   }
 
   public void setPatchLineComments(final List<PatchLineComment> plc) {
@@ -124,7 +124,7 @@
 
   private Repository getRepository() {
     try {
-      return server.openRepository(projectName);
+      return args.server.openRepository(projectName);
     } catch (RepositoryNotFoundException e) {
       return null;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index e3b65b8..ea57cde 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.reviewdb.AccountGroupMember;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -34,8 +34,9 @@
   }
 
   @Inject
-  public CreateChangeSender(@Assisted Change c) {
-    super(c);
+  public CreateChangeSender(EmailArguments ea, SshInfo sshInfo,
+      @Assisted Change c) {
+    super(ea, sshInfo, c);
   }
 
   @Override
@@ -46,38 +47,32 @@
   }
 
   private void bccWatchers() {
-    if (db != null) {
-      try {
-        // BCC anyone else who has interest in this project's changes
-        //
-        final ProjectState ps = getProjectState();
-        if (ps != null) {
-          // Try to mark interested owners with a TO and not a BCC line.
-          //
-          final Set<Account.Id> owners = new HashSet<Account.Id>();
-          for (AccountGroup.Id g : getProjectOwners()) {
-            for (AccountGroupMember m : db.accountGroupMembers().byGroup(g)) {
-              owners.add(m.getAccountId());
-            }
-          }
+    try {
+      // Try to mark interested owners with a TO and not a BCC line.
+      //
+      final Set<Account.Id> owners = new HashSet<Account.Id>();
+      for (AccountGroup.Id g : getProjectOwners()) {
+        for (AccountGroupMember m : args.db.get().accountGroupMembers()
+            .byGroup(g)) {
+          owners.add(m.getAccountId());
+        }
+      }
 
-          // BCC anyone who has interest in this project's changes
-          //
-          for (final AccountProjectWatch w : getProjectWatches()) {
-            if (w.isNotifyNewChanges()) {
-              if (owners.contains(w.getAccountId())) {
-                add(RecipientType.TO, w.getAccountId());
-              } else {
-                add(RecipientType.BCC, w.getAccountId());
-              }
-            }
+      // BCC anyone who has interest in this project's changes
+      //
+      for (final AccountProjectWatch w : getWatches()) {
+        if (w.isNotifyNewChanges()) {
+          if (owners.contains(w.getAccountId())) {
+            add(RecipientType.TO, w.getAccountId());
+          } else {
+            add(RecipientType.BCC, w.getAccountId());
           }
         }
-      } catch (OrmException err) {
-        // Just don't CC everyone. Better to send a partial message to those
-        // we already have queued up then to fail deliver entirely to people
-        // who have a lower interest in the change.
       }
+    } catch (OrmException err) {
+      // Just don't CC everyone. Better to send a partial message to those
+      // we already have queued up then to fail deliver entirely to people
+      // who have a lower interest in the change.
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
new file mode 100644
index 0000000..8a7ffd6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2010 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.mail;
+
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import javax.annotation.Nullable;
+
+class EmailArguments {
+  final GitRepositoryManager server;
+  final ProjectCache projectCache;
+  final AccountCache accountCache;
+  final PatchListCache patchListCache;
+  final FromAddressGenerator fromAddressGenerator;
+  final EmailSender emailSender;
+  final PatchSetInfoFactory patchSetInfoFactory;
+  final IdentifiedUser.GenericFactory identifiedUserFactory;
+  final Provider<String> urlProvider;
+  final Project.NameKey wildProject;
+
+  final ChangeQueryBuilder.Factory queryBuilder;
+  final Provider<ChangeQueryRewriter> queryRewriter;
+  final Provider<ReviewDb> db;
+
+  @Inject
+  EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
+      AccountCache accountCache, PatchListCache patchListCache,
+      FromAddressGenerator fromAddressGenerator, EmailSender emailSender,
+      PatchSetInfoFactory patchSetInfoFactory,
+      GenericFactory identifiedUserFactory,
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      @WildProjectName Project.NameKey wildProject,
+      ChangeQueryBuilder.Factory queryBuilder,
+      Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db) {
+    this.server = server;
+    this.projectCache = projectCache;
+    this.accountCache = accountCache;
+    this.patchListCache = patchListCache;
+    this.fromAddressGenerator = fromAddressGenerator;
+    this.emailSender = emailSender;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.urlProvider = urlProvider;
+    this.wildProject = wildProject;
+    this.queryBuilder = queryBuilder;
+    this.queryRewriter = queryRewriter;
+    this.db = db;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
index 11a541b..1395d78 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
@@ -25,8 +25,8 @@
   }
 
   @Inject
-  public MergeFailSender(@Assisted Change c) {
-    super(c, "comment");
+  public MergeFailSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "comment");
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index c467bf8..ee31c7e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -37,15 +37,14 @@
     public MergedSender create(Change change);
   }
 
+  private final ApprovalTypes approvalTypes;
   private Branch.NameKey dest;
 
   @Inject
-  private ApprovalTypes approvalTypes;
-
-  @Inject
-  public MergedSender(@Assisted Change c) {
-    super(c, "merged");
+  public MergedSender(EmailArguments ea, ApprovalTypes at, @Assisted Change c) {
+    super(ea, c, "merged");
     dest = c.getDest();
+    approvalTypes = at;
   }
 
   public void setDest(final Branch.NameKey key) {
@@ -78,7 +77,7 @@
   }
 
   private void formatApprovals() {
-    if (db != null && patchSet != null) {
+    if (patchSet != null) {
       try {
         final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> pos =
             new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
@@ -86,8 +85,8 @@
         final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> neg =
             new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
 
-        for (PatchSetApproval ca : db.patchSetApprovals().byPatchSet(
-            patchSet.getId())) {
+        for (PatchSetApproval ca : args.db.get().patchSetApprovals()
+            .byPatchSet(patchSet.getId())) {
           if (ca.getValue() > 0) {
             insert(pos, ca);
           } else if (ca.getValue() < 0) {
@@ -157,23 +156,18 @@
   }
 
   private void bccWatchesNotifySubmittedChanges() {
-    if (db != null) {
-      try {
-        // BCC anyone else who has interest in this project's changes
-        //
-        final ProjectState ps = getProjectState();
-        if (ps != null) {
-          for (final AccountProjectWatch w : getProjectWatches()) {
-            if (w.isNotifySubmittedChanges()) {
-              add(RecipientType.BCC, w.getAccountId());
-            }
-          }
+    try {
+      // BCC anyone else who has interest in this project's changes
+      //
+      for (final AccountProjectWatch w : getWatches()) {
+        if (w.isNotifySubmittedChanges()) {
+          add(RecipientType.BCC, w.getAccountId());
         }
-      } catch (OrmException err) {
-        // Just don't CC everyone. Better to send a partial message to those
-        // we already have queued up then to fail deliver entirely to people
-        // who have a lower interest in the change.
       }
+    } catch (OrmException err) {
+      // Just don't CC everyone. Better to send a partial message to those
+      // we already have queued up then to fail deliver entirely to people
+      // who have a lower interest in the change.
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 6c35f0e..459fc4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.server.ssh.SshInfo;
-import com.google.inject.Inject;
 
 import com.jcraft.jsch.HostKey;
 
@@ -29,14 +28,13 @@
 
 /** Sends an email alerting a user to a new change for them to review. */
 public abstract class NewChangeSender extends OutgoingEmail {
-  @Inject
-  private SshInfo sshInfo;
-
+  private final SshInfo sshInfo;
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
   private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
 
-  protected NewChangeSender(Change c) {
-    super(c, "newchange");
+  protected NewChangeSender(EmailArguments ea, SshInfo sshInfo, Change c) {
+    super(ea, c, "newchange");
+    this.sshInfo = sshInfo;
   }
 
   public void addReviewers(final Collection<Account.Id> cc) {
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 6d042ea..3e05c5f 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
@@ -22,27 +22,20 @@
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.StarredChange;
 import com.google.gerrit.reviewdb.UserIdentity;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.WildProjectName;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.EmailHeader.AddressList;
 import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+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.ChangeQueryBuilder;
 import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.eclipse.jgit.util.SystemReader;
 import org.slf4j.Logger;
@@ -63,8 +56,6 @@
 import java.util.Set;
 import java.util.TreeSet;
 
-import javax.annotation.Nullable;
-
 /** Sends an email to one or more interested parties. */
 public abstract class OutgoingEmail {
   private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class);
@@ -83,55 +74,24 @@
   private StringBuilder body;
   private boolean inFooter;
 
+  protected final EmailArguments args;
   protected Account.Id fromId;
   protected PatchSet patchSet;
   protected PatchSetInfo patchSetInfo;
   protected ChangeMessage changeMessage;
-  protected ReviewDb db;
-
-  @Inject
-  protected GitRepositoryManager server;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AccountCache accountCache;
-
-  @Inject
-  private PatchListCache patchListCache;
-
-  @Inject
-  private FromAddressGenerator fromAddressGenerator;
-
-  @Inject
-  private EmailSender emailSender;
-
-  @Inject
-  private PatchSetInfoFactory patchSetInfoFactory;
-
-  @Inject
-  private IdentifiedUser.GenericFactory identifiedUserFactory;
-
-  @Inject
-  @CanonicalWebUrl
-  @Nullable
-  private Provider<String> urlProvider;
-
-  @Inject
-  @WildProjectName
-  private Project.NameKey wildProject;
 
   private ProjectState projectState;
+  private ChangeData changeData;
 
-  protected OutgoingEmail(final Change c, final String mc) {
+  protected OutgoingEmail(EmailArguments ea, final Change c, final String mc) {
+    args = ea;
     change = c;
     messageClass = mc;
     headers = new LinkedHashMap<String, EmailHeader>();
   }
 
-  protected OutgoingEmail(final String mc) {
-    this(null, mc);
+  protected OutgoingEmail(EmailArguments ea, final String mc) {
+    this(ea, null, mc);
   }
 
   public void setFrom(final Account.Id id) {
@@ -151,17 +111,13 @@
     changeMessage = cm;
   }
 
-  public void setReviewDb(final ReviewDb d) {
-    db = d;
-  }
-
   /**
    * Format and enqueue the message for delivery.
    *
    * @throws EmailException
    */
   public void send() throws EmailException {
-    if (!emailSender.isEnabled()) {
+    if (!args.emailSender.isEnabled()) {
       // Server has explicitly disabled email sending.
       //
       return;
@@ -171,7 +127,7 @@
     format();
     if (shouldSendMessage()) {
       if (fromId != null) {
-        final Account fromUser = accountCache.get(fromId).getAccount();
+        final Account fromUser = args.accountCache.get(fromId).getAccount();
 
         if (fromUser.getGeneralPreferences().isCopySelfOnEmails()) {
           // If we are impersonating a user, make sure they receive a CC of
@@ -226,24 +182,22 @@
         appendText("Gerrit-Branch: " + change.getDest().getShortName() + "\n");
         appendText("Gerrit-Owner: " + getNameEmailFor(change.getOwner()) + "\n");
 
-        if (db != null) {
-          try {
-            HashSet<Account.Id> reviewers = new HashSet<Account.Id>();
-            for (PatchSetApproval p : db.patchSetApprovals().byChange(
-                change.getId())) {
-              reviewers.add(p.getAccountId());
-            }
-
-            TreeSet<String> names = new TreeSet<String>();
-            for (Account.Id who : reviewers) {
-              names.add(getNameEmailFor(who));
-            }
-
-            for (String name : names) {
-              appendText("Gerrit-Reviewer: " + name + "\n");
-            }
-          } catch (OrmException e) {
+        try {
+          HashSet<Account.Id> reviewers = new HashSet<Account.Id>();
+          for (PatchSetApproval p : args.db.get().patchSetApprovals().byChange(
+              change.getId())) {
+            reviewers.add(p.getAccountId());
           }
+
+          TreeSet<String> names = new TreeSet<String>();
+          for (Account.Id who : reviewers) {
+            names.add(getNameEmailFor(who));
+          }
+
+          for (String name : names) {
+            appendText("Gerrit-Reviewer: " + name + "\n");
+          }
+        } catch (OrmException e) {
         }
       }
 
@@ -259,7 +213,7 @@
         setHeader("Message-ID", rndid.toString());
       }
 
-      emailSender.send(smtpFromAddress, smtpRcptTo, headers, body.toString());
+      args.emailSender.send(smtpFromAddress, smtpRcptTo, headers, body.toString());
     }
   }
 
@@ -268,16 +222,18 @@
 
   /** Setup the message headers and envelope (TO, CC, BCC). */
   protected void init() {
-    if (change != null && projectCache != null) {
-      projectState = projectCache.get(change.getProject());
+    if (change != null && args.projectCache != null) {
+      changeData = new ChangeData(change);
+      projectState = args.projectCache.get(change.getProject());
       projectName =
           projectState != null ? projectState.getProject().getName() : null;
     } else {
+      changeData = null;
       projectState = null;
       projectName = null;
     }
 
-    smtpFromAddress = fromAddressGenerator.from(fromId);
+    smtpFromAddress = args.fromAddressGenerator.from(fromId);
     if (changeMessage != null && changeMessage.getWrittenOn() != null) {
       setHeader("Date", new Date(changeMessage.getWrittenOn().getTime()));
     } else {
@@ -313,8 +269,8 @@
     body = new StringBuilder();
     inFooter = false;
 
-    if (fromId != null && fromAddressGenerator.isGenericAddress(fromId)) {
-      final Account account = accountCache.get(fromId).getAccount();
+    if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) {
+      final Account account = args.accountCache.get(fromId).getAccount();
       final String name = account.getFullName();
       final String email = account.getPreferredEmail();
 
@@ -331,10 +287,10 @@
       }
     }
 
-    if (change != null && db != null) {
+    if (change != null) {
       if (patchSet == null) {
         try {
-          patchSet = db.patchSets().get(change.currentPatchSetId());
+          patchSet = args.db.get().patchSets().get(change.currentPatchSetId());
         } catch (OrmException err) {
           patchSet = null;
         }
@@ -342,7 +298,7 @@
 
       if (patchSet != null && patchSetInfo == null) {
         try {
-          patchSetInfo = patchSetInfoFactory.get(patchSet.getId());
+          patchSetInfo = args.patchSetInfoFactory.get(patchSet.getId());
         } catch (PatchSetInfoNotAvailableException err) {
           patchSetInfo = null;
         }
@@ -439,7 +395,7 @@
   }
 
   protected String getGerritUrl() {
-    return urlProvider.get();
+    return args.urlProvider.get();
   }
 
   protected String getChangeMessageThreadId() {
@@ -521,7 +477,7 @@
   /** Get the patch list corresponding to this patch set. */
   protected PatchList getPatchList() {
     if (patchSet != null) {
-      return patchListCache.get(change, patchSet);
+      return args.patchListCache.get(change, patchSet);
     }
     return null;
   }
@@ -532,7 +488,7 @@
       return "Anonymous Coward";
     }
 
-    final Account userAccount = accountCache.get(accountId).getAccount();
+    final Account userAccount = args.accountCache.get(accountId).getAccount();
     String name = userAccount.getFullName();
     if (name == null) {
       name = userAccount.getPreferredEmail();
@@ -544,7 +500,7 @@
   }
 
   private String getNameEmailFor(Account.Id accountId) {
-    AccountState who = accountCache.get(accountId);
+    AccountState who = args.accountCache.get(accountId);
     String name = who.getAccount().getFullName();
     String email = who.getAccount().getPreferredEmail();
 
@@ -594,7 +550,7 @@
   protected Set<AccountGroup.Id> getProjectOwners() {
     final ProjectState r;
 
-    r = projectCache.get(change.getProject());
+    r = args.projectCache.get(change.getProject());
     return r != null ? r.getOwners() : Collections.<AccountGroup.Id> emptySet();
   }
 
@@ -625,62 +581,82 @@
 
   /** BCC any user who has starred this change. */
   protected void bccStarredBy() {
-    if (db != null) {
-      try {
-        // BCC anyone who has starred this change.
-        //
-        for (StarredChange w : db.starredChanges().byChange(change.getId())) {
-          add(RecipientType.BCC, w.getAccountId());
-        }
-      } catch (OrmException err) {
-        // Just don't BCC everyone. Better to send a partial message to those
-        // we already have queued up then to fail deliver entirely to people
-        // who have a lower interest in the change.
+    try {
+      // BCC anyone who has starred this change.
+      //
+      for (StarredChange w : args.db.get().starredChanges().byChange(
+          change.getId())) {
+        add(RecipientType.BCC, w.getAccountId());
       }
+    } catch (OrmException err) {
+      // Just don't BCC everyone. Better to send a partial message to those
+      // we already have queued up then to fail deliver entirely to people
+      // who have a lower interest in the change.
     }
   }
 
   /** BCC any user who has set "notify all comments" on this project. */
   protected void bccWatchesNotifyAllComments() {
-    if (db != null) {
-      try {
-        // BCC anyone else who has interest in this project's changes
-        //
-        final ProjectState ps = getProjectState();
-        if (ps != null) {
-          for (final AccountProjectWatch w : getProjectWatches()) {
-            if (w.isNotifyAllComments()) {
-              add(RecipientType.BCC, w.getAccountId());
-            }
-          }
+    try {
+      // BCC anyone else who has interest in this project's changes
+      //
+      for (final AccountProjectWatch w : getWatches()) {
+        if (w.isNotifyAllComments()) {
+          add(RecipientType.BCC, w.getAccountId());
         }
-      } catch (OrmException err) {
-        // Just don't CC everyone. Better to send a partial message to those
-        // we already have queued up then to fail deliver entirely to people
-        // who have a lower interest in the change.
       }
+    } catch (OrmException err) {
+      // Just don't CC everyone. Better to send a partial message to those
+      // we already have queued up then to fail deliver entirely to people
+      // who have a lower interest in the change.
     }
   }
 
-  /** Returns all watches that are relevant for this project */
-  final protected Set<AccountProjectWatch> getProjectWatches() throws OrmException {
-    final Set<AccountProjectWatch> projectWatches = new HashSet<AccountProjectWatch>();
-    final Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
-    final ProjectState ps = getProjectState();
-    if (ps != null) {
-      for (final AccountProjectWatch w : db.accountProjectWatches().byProject(ps.getProject().getNameKey())) {
-        projectWatches.add(w);
-        projectWatchers.add(w.getAccountId());
-      }
+  /** Returns all watches that are relevant */
+  protected final List<AccountProjectWatch> getWatches() throws OrmException {
+    if (changeData == null) {
+      return Collections.emptyList();
     }
-    for (final AccountProjectWatch w : db.accountProjectWatches().byProject(wildProject)) {
+
+    List<AccountProjectWatch> matching = new ArrayList<AccountProjectWatch>();
+    Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
+
+    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+        .byProject(change.getProject())) {
+      projectWatchers.add(w.getAccountId());
+      add(matching, w);
+    }
+
+    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+        .byProject(args.wildProject)) {
       if (!projectWatchers.contains(w.getAccountId())) {
-        // the all projects watch settings are only relevant if the user did not configure
-        // any specific rules for the concrete project
-        projectWatches.add(w);
+        add(matching, w);
       }
     }
-    return Collections.unmodifiableSet(projectWatches);
+
+    return Collections.unmodifiableList(matching);
+  }
+
+  @SuppressWarnings("unchecked")
+  private void add(List<AccountProjectWatch> matching, AccountProjectWatch w)
+      throws OrmException {
+    IdentifiedUser user =
+        args.identifiedUserFactory.create(args.db, w.getAccountId());
+    ChangeQueryBuilder qb = args.queryBuilder.create(user);
+    Predicate<ChangeData> p = qb.is_visible();
+    if (w.getFilter() != null) {
+      try {
+        p = Predicate.and(qb.parse(w.getFilter()), p);
+        p = args.queryRewriter.get().rewrite(p);
+        if (p.match(changeData)) {
+          matching.add(w);
+        }
+      } catch (QueryParseException e) {
+        // Ignore broken filter expressions.
+      }
+    } else if (p.match(changeData)) {
+      matching.add(w);
+    }
   }
 
   /** Any user who has published comments on this change. */
@@ -694,19 +670,17 @@
   }
 
   private void ccApprovals(final boolean includeZero) {
-    if (db != null) {
-      try {
-        // CC anyone else who has posted an approval mark on this change
-        //
-        for (PatchSetApproval ap : db.patchSetApprovals().byChange(
-            change.getId())) {
-          if (!includeZero && ap.getValue() == 0) {
-            continue;
-          }
-          add(RecipientType.CC, ap.getAccountId());
+    try {
+      // CC anyone else who has posted an approval mark on this change
+      //
+      for (PatchSetApproval ap : args.db.get().patchSetApprovals().byChange(
+          change.getId())) {
+        if (!includeZero && ap.getValue() == 0) {
+          continue;
         }
-      } catch (OrmException err) {
+        add(RecipientType.CC, ap.getAccountId());
       }
+    } catch (OrmException err) {
     }
   }
 
@@ -721,14 +695,14 @@
   private boolean isVisibleTo(final Account.Id to) {
     return projectState == null
         || change == null
-        || projectState.controlFor(identifiedUserFactory.create(to))
+        || projectState.controlFor(args.identifiedUserFactory.create(to))
             .controlFor(change).isVisible();
   }
 
   /** Schedule delivery of this message to the given account. */
   protected void add(final RecipientType rt, final Address addr) {
     if (addr != null && addr.email != null && addr.email.length() > 0) {
-      if (emailSender.canEmail(addr.email)) {
+      if (args.emailSender.canEmail(addr.email)) {
         smtpRcptTo.add(addr);
         switch (rt) {
           case TO:
@@ -745,7 +719,7 @@
   }
 
   private Address toAddress(final Account.Id id) {
-    final Account a = accountCache.get(id).getAccount();
+    final Account a = args.accountCache.get(id).getAccount();
     final String e = a.getPreferredEmail();
     if (e == null) {
       return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
index d22cc59..0007efe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -28,14 +28,14 @@
     public RegisterNewEmailSender create(String address);
   }
 
+  private final AuthConfig authConfig;
   private final String addr;
 
   @Inject
-  private AuthConfig authConfig;
-
-  @Inject
-  public RegisterNewEmailSender(@Assisted final String address) {
-    super("registernewemail");
+  public RegisterNewEmailSender(EmailArguments ea, AuthConfig ac,
+      @Assisted final String address) {
+    super(ea, "registernewemail");
+    authConfig = ac;
     addr = address;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
index 70ed4f5..91cd400 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
@@ -34,15 +34,14 @@
     public ReplacePatchSetSender create(Change change);
   }
 
-  @Inject
-  private SshInfo sshInfo;
-
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
   private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
+  private final SshInfo sshInfo;
 
   @Inject
-  public ReplacePatchSetSender(@Assisted Change c) {
-    super(c, "newpatchset");
+  public ReplacePatchSetSender(EmailArguments ea, SshInfo si, @Assisted Change c) {
+    super(ea, c, "newpatchset");
+    sshInfo = si;
   }
 
   public void addReviewers(final Collection<Account.Id> cc) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
index 99e9565..9163dc3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
@@ -18,8 +18,8 @@
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
 public abstract class ReplyToChangeSender extends OutgoingEmail {
-  protected ReplyToChangeSender(Change c, String mc) {
-    super(c, mc);
+  protected ReplyToChangeSender(EmailArguments ea, Change c, String mc) {
+    super(ea, c, mc);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
index 6fdb06e..f3e2890 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
@@ -284,7 +284,6 @@
       cm.setPatchSet(patchSet, patchSetInfoFactory.get(patchSetId));
       cm.setChangeMessage(message);
       cm.setPatchLineComments(drafts);
-      cm.setReviewDb(db);
       cm.send();
     } catch (EmailException e) {
       log.error("Cannot send comments by email for patch set " + patchSetId, e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index 8e006f3..a0aeb58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -26,8 +26,6 @@
 import static com.google.gerrit.server.query.QueryParser.SINGLE_WORD;
 import static com.google.gerrit.server.query.QueryParser.VARIABLE_ASSIGN;
 
-import com.google.gerrit.server.query.change.ChangeData;
-
 import org.antlr.runtime.tree.Tree;
 
 import java.lang.annotation.ElementType;
@@ -244,6 +242,29 @@
    *
    * @param p the predicate to find.
    * @param clazz type of the predicate instance.
+   * @return the predicate, null if not found.
+   */
+  @SuppressWarnings("unchecked")
+  public <P extends Predicate<T>> P find(Predicate<T> p, Class<P> clazz) {
+    if (clazz.isAssignableFrom(p.getClass())) {
+      return (P) p;
+    }
+
+    for (Predicate<T> c : p.getChildren()) {
+      P r = find(c, clazz);
+      if (r != null) {
+        return r;
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Locate a predicate in the predicate tree.
+   *
+   * @param p the predicate to find.
+   * @param clazz type of the predicate instance.
    * @param name name of the operator.
    * @return the predicate, null if not found.
    */
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 9be4bf1..b704c1a 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
@@ -35,7 +35,7 @@
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.servlet.RequestScoped;
+import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 
@@ -46,7 +46,6 @@
 /**
  * Parses a query string meant to be applied to change objects.
  */
-@RequestScoped
 public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
   private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
   private static final Pattern PAT_CHANGE_ID =
@@ -86,55 +85,67 @@
       new QueryBuilder.Definition<ChangeData, ChangeQueryBuilder>(
           ChangeQueryBuilder.class);
 
-  private final Provider<ReviewDb> dbProvider;
-  private final Provider<CurrentUser> currentUser;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final ChangeControl.Factory changeControlFactory;
-  private final AccountResolver accountResolver;
-  private final GroupCache groupCache;
-  private final AuthConfig authConfig;
-  private final ApprovalTypes approvalTypes;
-  private final Project.NameKey wildProjectName;
+  static class Arguments {
+    final Provider<ReviewDb> dbProvider;
+    final Provider<ChangeQueryRewriter> rewriter;
+    final IdentifiedUser.GenericFactory userFactory;
+    final ChangeControl.Factory changeControlFactory;
+    final AccountResolver accountResolver;
+    final GroupCache groupCache;
+    final AuthConfig authConfig;
+    final ApprovalTypes approvalTypes;
+    final Project.NameKey wildProjectName;
 
-  @Inject
-  ChangeQueryBuilder(Provider<ReviewDb> dbProvider,
-      Provider<CurrentUser> currentUser,
-      IdentifiedUser.GenericFactory userFactory,
-      ChangeControl.Factory changeControlFactory,
-      AccountResolver accountResolver, GroupCache groupCache,
-      AuthConfig authConfig, ApprovalTypes approvalTypes,
-      @WildProjectName Project.NameKey wildProjectName) {
-    super(mydef);
-    this.dbProvider = dbProvider;
-    this.currentUser = currentUser;
-    this.userFactory = userFactory;
-    this.changeControlFactory = changeControlFactory;
-    this.accountResolver = accountResolver;
-    this.groupCache = groupCache;
-    this.authConfig = authConfig;
-    this.approvalTypes = approvalTypes;
-    this.wildProjectName = wildProjectName;
+    @Inject
+    Arguments(Provider<ReviewDb> dbProvider,
+        Provider<ChangeQueryRewriter> rewriter,
+        IdentifiedUser.GenericFactory userFactory,
+        ChangeControl.Factory changeControlFactory,
+        AccountResolver accountResolver, GroupCache groupCache,
+        AuthConfig authConfig, ApprovalTypes approvalTypes,
+        @WildProjectName Project.NameKey wildProjectName) {
+      this.dbProvider = dbProvider;
+      this.rewriter = rewriter;
+      this.userFactory = userFactory;
+      this.changeControlFactory = changeControlFactory;
+      this.accountResolver = accountResolver;
+      this.groupCache = groupCache;
+      this.authConfig = authConfig;
+      this.approvalTypes = approvalTypes;
+      this.wildProjectName = wildProjectName;
+    }
   }
 
-  Provider<ReviewDb> getReviewDbProvider() {
-    return dbProvider;
+  public interface Factory {
+    ChangeQueryBuilder create(CurrentUser user);
+  }
+
+  private final Arguments args;
+  private final CurrentUser currentUser;
+
+  @Inject
+  ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
+    super(mydef);
+    this.args = args;
+    this.currentUser = currentUser;
   }
 
   @Operator
   public Predicate<ChangeData> age(String value) {
-    return new AgePredicate(dbProvider, value);
+    return new AgePredicate(args.dbProvider, value);
   }
 
   @Operator
   public Predicate<ChangeData> change(String query) {
     if (PAT_LEGACY_ID.matcher(query).matches()) {
-      return new LegacyChangeIdPredicate(dbProvider, Change.Id.parse(query));
+      return new LegacyChangeIdPredicate(args.dbProvider, Change.Id
+          .parse(query));
 
     } else if (PAT_CHANGE_ID.matcher(query).matches()) {
       if (query.charAt(0) == 'i') {
         query = "I" + query.substring(1);
       }
-      return new ChangeIdPredicate(dbProvider, query);
+      return new ChangeIdPredicate(args.dbProvider, query);
     }
 
     throw new IllegalArgumentException();
@@ -146,30 +157,30 @@
       return status_open();
 
     } else if ("closed".equals(statusName)) {
-      return ChangeStatusPredicate.closed(dbProvider);
+      return ChangeStatusPredicate.closed(args.dbProvider);
 
     } else if ("reviewed".equalsIgnoreCase(statusName)) {
-      return new IsReviewedPredicate(dbProvider);
+      return new IsReviewedPredicate(args.dbProvider);
 
     } else {
-      return new ChangeStatusPredicate(dbProvider, statusName);
+      return new ChangeStatusPredicate(args.dbProvider, statusName);
     }
   }
 
   public Predicate<ChangeData> status_open() {
-    return ChangeStatusPredicate.open(dbProvider);
+    return ChangeStatusPredicate.open(args.dbProvider);
   }
 
   @Operator
   public Predicate<ChangeData> has(String value) {
     if ("star".equalsIgnoreCase(value)) {
-      return new IsStarredByPredicate(dbProvider, currentUser.get());
+      return new IsStarredByPredicate(args.dbProvider, currentUser);
     }
 
     if ("draft".equalsIgnoreCase(value)) {
-      if (currentUser.get() instanceof IdentifiedUser) {
-        return new HasDraftByPredicate(dbProvider,
-            ((IdentifiedUser) currentUser.get()).getAccountId());
+      if (currentUser instanceof IdentifiedUser) {
+        return new HasDraftByPredicate(args.dbProvider,
+            ((IdentifiedUser) currentUser).getAccountId());
       }
     }
 
@@ -179,21 +190,19 @@
   @Operator
   public Predicate<ChangeData> is(String value) {
     if ("starred".equalsIgnoreCase(value)) {
-      return new IsStarredByPredicate(dbProvider, currentUser.get());
+      return new IsStarredByPredicate(args.dbProvider, currentUser);
     }
 
     if ("watched".equalsIgnoreCase(value)) {
-      return new IsWatchedByPredicate(dbProvider, wildProjectName, //
-          currentUser.get());
+      return new IsWatchedByPredicate(args, currentUser);
     }
 
     if ("visible".equalsIgnoreCase(value)) {
-      return new IsVisibleToPredicate(dbProvider, changeControlFactory,
-          currentUser.get());
+      return is_visible();
     }
 
     if ("reviewed".equalsIgnoreCase(value)) {
-      return new IsReviewedPredicate(dbProvider);
+      return new IsReviewedPredicate(args.dbProvider);
     }
 
     try {
@@ -207,121 +216,129 @@
 
   @Operator
   public Predicate<ChangeData> commit(String id) {
-    return new CommitPredicate(dbProvider, AbbreviatedObjectId.fromString(id));
+    return new CommitPredicate(args.dbProvider, AbbreviatedObjectId
+        .fromString(id));
   }
 
   @Operator
   public Predicate<ChangeData> project(String name) {
-    return new ProjectPredicate(dbProvider, name);
+    return new ProjectPredicate(args.dbProvider, name);
   }
 
   @Operator
   public Predicate<ChangeData> branch(String name) {
-    return new BranchPredicate(dbProvider, name);
+    return new BranchPredicate(args.dbProvider, name);
   }
 
   @Operator
   public Predicate<ChangeData> topic(String name) {
-    return new TopicPredicate(dbProvider, name);
+    return new TopicPredicate(args.dbProvider, name);
   }
 
   @Operator
   public Predicate<ChangeData> ref(String ref) {
-    return new RefPredicate(dbProvider, ref);
+    return new RefPredicate(args.dbProvider, ref);
   }
 
   @Operator
   public Predicate<ChangeData> label(String name) {
-    return new LabelPredicate(dbProvider, approvalTypes, name);
+    return new LabelPredicate(args.dbProvider, args.approvalTypes, name);
   }
 
   @Operator
   public Predicate<ChangeData> starredby(String who)
       throws QueryParseException, OrmException {
-    Account account = accountResolver.find(who);
+    Account account = args.accountResolver.find(who);
     if (account == null) {
       throw error("User " + who + " not found");
     }
-    return new IsStarredByPredicate(dbProvider, //
-        userFactory.create(dbProvider, account.getId()));
+    return new IsStarredByPredicate(args.dbProvider, //
+        args.userFactory.create(args.dbProvider, account.getId()));
   }
 
   @Operator
   public Predicate<ChangeData> watchedby(String who)
       throws QueryParseException, OrmException {
-    Account account = accountResolver.find(who);
+    Account account = args.accountResolver.find(who);
     if (account == null) {
       throw error("User " + who + " not found");
     }
-    return new IsWatchedByPredicate(dbProvider, wildProjectName, //
-        userFactory.create(dbProvider, account.getId()));
+    return new IsWatchedByPredicate(args, args.userFactory.create(
+        args.dbProvider, account.getId()));
   }
 
   @Operator
   public Predicate<ChangeData> draftby(String who) throws QueryParseException,
       OrmException {
-    Account account = accountResolver.find(who);
+    Account account = args.accountResolver.find(who);
     if (account == null) {
       throw error("User " + who + " not found");
     }
-    return new HasDraftByPredicate(dbProvider, account.getId());
+    return new HasDraftByPredicate(args.dbProvider, account.getId());
   }
 
   @Operator
   public Predicate<ChangeData> visibleto(String who)
       throws QueryParseException, OrmException {
-    Account account = accountResolver.find(who);
+    Account account = args.accountResolver.find(who);
     if (account != null) {
-      return visibleto(userFactory.create(dbProvider, account.getId()));
+      return visibleto(args.userFactory
+          .create(args.dbProvider, account.getId()));
     }
 
     // If its not an account, maybe its a group?
     //
-    AccountGroup g = groupCache.get(new AccountGroup.NameKey(who));
+    AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
     if (g != null) {
-      return visibleto(new SingleGroupUser(authConfig, g.getId()));
+      return visibleto(new SingleGroupUser(args.authConfig, g.getId()));
     }
 
     Collection<AccountGroup> matches =
-        groupCache.get(new AccountGroup.ExternalNameKey(who));
+        args.groupCache.get(new AccountGroup.ExternalNameKey(who));
     if (matches != null && !matches.isEmpty()) {
       HashSet<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
       for (AccountGroup group : matches) {
         ids.add(group.getId());
       }
-      return visibleto(new SingleGroupUser(authConfig, ids));
+      return visibleto(new SingleGroupUser(args.authConfig, ids));
     }
 
     throw error("No user or group matches \"" + who + "\".");
   }
 
   public Predicate<ChangeData> visibleto(CurrentUser user) {
-    return new IsVisibleToPredicate(dbProvider, changeControlFactory, user);
+    return new IsVisibleToPredicate(args.dbProvider, //
+        args.changeControlFactory, //
+        user);
+  }
+
+  public Predicate<ChangeData> is_visible() {
+    return visibleto(currentUser);
   }
 
   @Operator
   public Predicate<ChangeData> owner(String who) throws QueryParseException,
       OrmException {
-    Account account = accountResolver.find(who);
+    Account account = args.accountResolver.find(who);
     if (account == null) {
       throw error("User " + who + " not found");
     }
-    return new OwnerPredicate(dbProvider, account.getId());
+    return new OwnerPredicate(args.dbProvider, account.getId());
   }
 
   @Operator
   public Predicate<ChangeData> reviewer(String nameOrEmail)
       throws QueryParseException, OrmException {
-    Account account = accountResolver.find(nameOrEmail);
+    Account account = args.accountResolver.find(nameOrEmail);
     if (account == null) {
       throw error("Reviewer " + nameOrEmail + " not found");
     }
-    return new ReviewerPredicate(dbProvider, account.getId());
+    return new ReviewerPredicate(args.dbProvider, account.getId());
   }
 
   @Operator
   public Predicate<ChangeData> tr(String trackingId) {
-    return new TrackingIdPredicate(dbProvider, trackingId);
+    return new TrackingIdPredicate(args.dbProvider, trackingId);
   }
 
   @Operator
@@ -350,12 +367,12 @@
 
   @Operator
   public Predicate<ChangeData> sortkey_after(String sortKey) {
-    return new SortKeyPredicate.After(dbProvider, sortKey);
+    return new SortKeyPredicate.After(args.dbProvider, sortKey);
   }
 
   @Operator
   public Predicate<ChangeData> sortkey_before(String sortKey) {
-    return new SortKeyPredicate.Before(dbProvider, sortKey);
+    return new SortKeyPredicate.Before(args.dbProvider, sortKey);
   }
 
   @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 c25a753..ee09239 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
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.ChangeAccess;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.query.IntPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryRewriter;
@@ -29,18 +28,18 @@
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
-import com.google.inject.servlet.RequestScoped;
 
 import java.util.Collection;
 
-@RequestScoped
 public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
   private static final QueryRewriter.Definition<ChangeData, ChangeQueryRewriter> mydef =
       new QueryRewriter.Definition<ChangeData, ChangeQueryRewriter>(
           ChangeQueryRewriter.class, new ChangeQueryBuilder(
-              new InvalidProvider<ReviewDb>(),
-              new InvalidProvider<CurrentUser>(), //
-              null, null, null, null, null, null, null));
+              new ChangeQueryBuilder.Arguments( //
+                  new InvalidProvider<ReviewDb>(), //
+                  new InvalidProvider<ChangeQueryRewriter>(), //
+                  null, null, null, null, null, null, null),
+              null));
 
   private final Provider<ReviewDb> dbProvider;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 429b07b..870be73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -14,18 +14,20 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.WildProjectName;
 import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.client.OrmException;
-import com.google.inject.Provider;
 
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 class IsWatchedByPredicate extends OperatorPredicate<ChangeData> {
   private static String describe(CurrentUser user) {
@@ -35,32 +37,81 @@
     return user.toString();
   }
 
-  private final Provider<ReviewDb> db;
-  private final Project.NameKey wildProject;
+  private final ChangeQueryBuilder.Arguments args;
   private final CurrentUser user;
 
-  IsWatchedByPredicate(Provider<ReviewDb> db,
-      @WildProjectName Project.NameKey wildProject, CurrentUser user) {
+  private Map<Project.NameKey, List<Predicate<ChangeData>>> rules;
+
+  IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, CurrentUser user) {
     super(ChangeQueryBuilder.FIELD_WATCHEDBY, describe(user));
-    this.db = db;
-    this.wildProject = wildProject;
+    this.args = args;
     this.user = user;
   }
 
   @Override
   public boolean match(final ChangeData cd) throws OrmException {
-    Set<NameKey> watched = user.getWatchedProjects();
-    if (watched.contains(wildProject)) {
-      return true;
+    if (rules == null) {
+      ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
+      rules = new HashMap<Project.NameKey, List<Predicate<ChangeData>>>();
+      for (AccountProjectWatch w : user.getNotificationFilters()) {
+        List<Predicate<ChangeData>> list = rules.get(w.getProjectNameKey());
+        if (list == null) {
+          list = new ArrayList<Predicate<ChangeData>>(4);
+          rules.put(w.getProjectNameKey(), list);
+        }
+
+        Predicate<ChangeData> p = compile(builder, w);
+        if (p != null) {
+          list.add(p);
+        }
+      }
     }
 
-    Change change = cd.change(db);
+    if (rules.isEmpty()) {
+      return false;
+    }
+
+    Change change = cd.change(args.dbProvider);
     if (change == null) {
       return false;
     }
 
     Project.NameKey project = change.getDest().getParentKey();
-    return watched.contains(project);
+    List<Predicate<ChangeData>> list = rules.get(project);
+    if (list == null) {
+      list = rules.get(args.wildProjectName);
+    }
+    if (list != null) {
+      for (Predicate<ChangeData> p : list) {
+        if (p.match(cd)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Predicate<ChangeData> compile(ChangeQueryBuilder builder,
+      AccountProjectWatch w) {
+    Predicate<ChangeData> p = builder.is_visible();
+    if (w.getFilter() != null) {
+      try {
+        p = Predicate.and(builder.parse(w.getFilter()), p);
+        if (builder.find(p, IsWatchedByPredicate.class) != null) {
+          // If the query is going to infinite loop, assume it
+          // will never match and return null. Yes this test
+          // prevents you from having a filter that matches what
+          // another user is filtering on. :-)
+          //
+          return null;
+        }
+        p = args.rewriter.get().rewrite(p);
+      } catch (QueryParseException e) {
+        return null;
+      }
+    }
+    return p;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 3065fd1..62d3bbd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -60,7 +60,6 @@
   private final SimpleDateFormat sdf =
       new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
 
-  private final CurrentUser currentUser;
   private final EventFactory eventFactory;
   private final ChangeQueryBuilder queryBuilder;
   private final ChangeQueryRewriter queryRewriter;
@@ -75,12 +74,11 @@
   private PrintWriter out;
 
   @Inject
-  QueryProcessor(CurrentUser currentUser, EventFactory eventFactory,
-      ChangeQueryBuilder queryBuilder, ChangeQueryRewriter queryRewriter,
-      Provider<ReviewDb> db) {
-    this.currentUser = currentUser;
+  QueryProcessor(EventFactory eventFactory,
+      ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
+      ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db) {
     this.eventFactory = eventFactory;
-    this.queryBuilder = queryBuilder;
+    this.queryBuilder = queryBuilder.create(currentUser);
     this.queryRewriter = queryRewriter;
     this.db = db;
   }
@@ -107,9 +105,7 @@
         final QueryStats stats = new QueryStats();
         stats.runTimeMilliseconds = System.currentTimeMillis();
 
-        final Predicate<ChangeData> visibleToMe =
-            queryBuilder.visibleto(currentUser);
-
+        final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
         Predicate<ChangeData> s = compileQuery(queryString, visibleToMe);
         List<ChangeData> results = new ArrayList<ChangeData>();
         HashSet<Change.Id> want = new HashSet<Change.Id>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index 36e04d9..f8ab33f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AuthConfig;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
@@ -47,7 +49,7 @@
   }
 
   @Override
-  public Set<Project.NameKey> getWatchedProjects() {
+  public Collection<AccountProjectWatch> getNotificationFilters() {
     return Collections.emptySet();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index d2638b3..2f8d4ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  private static final Class<? extends SchemaVersion> C = Schema_39.class;
+  private static final Class<? extends SchemaVersion> C = Schema_40.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java
new file mode 100644
index 0000000..7d3e4f5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.AccountProjectWatch;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.DialectH2;
+import com.google.gwtorm.schema.sql.DialectMySQL;
+import com.google.gwtorm.schema.sql.DialectPostgreSQL;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_40 extends SchemaVersion {
+  @Inject
+  Schema_40(Provider<Schema_39> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException,
+      OrmException {
+    // Set to "*" the filter field of the previously watched projects
+    //
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.execute("UPDATE account_project_watches" //
+          + " SET filter = '" + AccountProjectWatch.FILTER_ALL + "'" //
+          + " WHERE filter IS NULL OR filter = ''");
+
+      // Set the new primary key
+      //
+      final SqlDialect dialect = ((JdbcSchema) db).getDialect();
+      if (dialect instanceof DialectPostgreSQL) {
+        stmt.execute("ALTER TABLE account_project_watches "
+            + "DROP CONSTRAINT account_project_watches_pkey");
+        stmt.execute("ALTER TABLE account_project_watches "
+            + "ADD PRIMARY KEY (account_id, project_name, filter)");
+
+      } else if ((dialect instanceof DialectH2)
+          || (dialect instanceof DialectMySQL)) {
+        stmt.execute("ALTER TABLE account_project_watches DROP PRIMARY KEY");
+        stmt.execute("ALTER TABLE account_project_watches "
+            + "ADD PRIMARY KEY (account_id, project_name, filter)");
+
+      } else {
+        throw new OrmException("Unsupported dialect " + dialect);
+      }
+    } finally {
+      stmt.close();
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 4bc3686..a0b9d25 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.project.CanSubmitResult;
@@ -96,6 +97,9 @@
   private MergeQueue merger;
 
   @Inject
+  private MergeOp.Factory opFactory;
+
+  @Inject
   private ApprovalTypes approvalTypes;
 
   @Inject
@@ -166,7 +170,7 @@
           changeControl.canSubmit(patchSetId, db, approvalTypes,
               functionStateFactory);
       if (result == CanSubmitResult.OK) {
-        ChangeUtil.submit(patchSetId, currentUser, db, merger);
+        ChangeUtil.submit(opFactory, patchSetId, currentUser, db, merger);
       } else {
         throw error(result.getMessage());
       }