Merge changes from topic 'rebuild-fixes'

* changes:
  ChangeNotesParser: Don't open the repository
  AbstractChangeNotes: Disallow null Change.Id
  Disable hashtag tests under GERRIT_NOTEDB=write
  NoteDb: Don't convert entities whose patch set is missing
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index ae42477..423d87d 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1437,6 +1437,142 @@
   }
 ----
 
+[[get-watched-projects]]
+=== Get Watched Projects
+--
+'GET /accounts/link:#account-id[\{account-id\}]/watched.projects'
+--
+
+Retrieves all projects a user is watching.
+
+.Request
+----
+  GET /a/accounts/self/watched.projects HTTP/1.0
+----
+
+As result the watched projects of the user are returned as a list of
+link:#project-watch-info[ProjectWatchInfo] entities.
+The result is sorted by project name in ascending order.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "project": "Test Project 1",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    },
+    {
+      "project": "Test Project 2",
+      "filter": "branch:experimental",
+      "notify_all_comments": true,
+      "notify_submitted_changes": true,
+      "notify_abandoned_changes": true
+    }
+  ]
+----
+
+[[set-watched-projects]]
+=== Add/Update a List of Watched Project Entities
+--
+'POST /accounts/link:#account-id[\{account-id\}]/watched.projects'
+--
+
+Add new projects to watch or update existing watched projects.
+Projects that are already watched by a user will be updated with
+the provided configuration. All other projects in the request
+will be watched using the provided configuration. The posted body
+can contain link:#project-watch-info[ProjectWatchInfo] entities.
+Omitted boolean values will be set to false.
+
+.Request
+----
+  POST /a/accounts/self/watched.projects HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  [
+    {
+      "project": "Test Project 1",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    }
+  ]
+----
+
+As result the watched projects of the user are returned as a list of
+link:#project-watch-info[ProjectWatchInfo] entities.
+The result is sorted by project name in ascending order.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "project": "Test Project 1",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    },
+    {
+      "project": "Test Project 2",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    }
+  ]
+----
+
+[[delete-watched-projects]]
+=== Delete Watched Projects
+--
+'POST /accounts/link:#account-id[\{account-id\}]/watched.projects:delete'
+--
+
+Projects posted to this endpoint will no longer be watched. The posted body
+can contain an array of project names as strings.
+
+.Request
+----
+  POST /a/accounts/self/watched.projects:delete HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  [
+    "Test Project 1"
+  ]
+----
+
+As result the watched projects of the user are returned as a list of
+link:#project-watch-info[ProjectWatchInfo] entities.
+The result is sorted by project name in ascending order.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "project": "Test Project 2",
+      "notify_new_changes": true,
+      "notify_new_patch_sets": true,
+      "notify_all_comments": true,
+    }
+  ]
+----
+
 [[get-starred-changes]]
 === Get Starred Changes
 --
@@ -2095,6 +2231,22 @@
 |`username` |The new username of the account.
 |=======================
 
+[[project-watch-info]]
+=== ProjectWatchInfo
+The `WatchedProjectsInfo` entity contains information about a project watch
+for a user.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name                 |        |Description
+|`project`                  |        |The name of the project.
+|`filter`                   |optional|A filter string to be applied to the project.
+|`notify_new_changes`       |optional|Notify on new changes.
+|`notify_new_patch_sets`    |optional|Notify on new patch sets.
+|`notify_all_comments`      |optional|Notify on comments.
+|`notify_submitted_changes` |optional|Notify on submitted changes.
+|`notify_abandoned_changes` |optional|Notify on abandoned changes.
+|=======================
 
 GERRIT
 ------
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
new file mode 100644
index 0000000..879430a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -0,0 +1,169 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.acceptance.rest.account;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class WatchedProjectsIT extends AbstractDaemonTest {
+
+  private static final String NEW_PROJECT_NAME = "newProjectAccess";
+
+  @Test
+  public void setAndGetWatchedProjects() throws Exception {
+    String projectName1 = createProject(NEW_PROJECT_NAME).get();
+    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(2);
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName1;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    pwi = new ProjectWatchInfo();
+    pwi.project = projectName2;
+    pwi.filter = "branch:master";
+    pwi.notifySubmittedChanges = true;
+    pwi.notifyNewPatchSets = true;
+    projectsToWatch.add(pwi);
+
+    List<ProjectWatchInfo> persistedWatchedProjects =
+        gApi.accounts().self().setWatchedProjects(projectsToWatch);
+    assertThat(persistedWatchedProjects)
+        .containsAllIn(projectsToWatch).inOrder();
+  }
+
+  @Test
+  public void setAndDeleteWatchedProjects() throws Exception {
+    String projectName1 = createProject(NEW_PROJECT_NAME).get();
+    String projectName2 = createProject(NEW_PROJECT_NAME + "2").get();
+
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName1;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    pwi = new ProjectWatchInfo();
+    pwi.project = projectName2;
+    pwi.filter = "branch:master";
+    pwi.notifySubmittedChanges = true;
+    pwi.notifyNewPatchSets = true;
+    projectsToWatch.add(pwi);
+
+    // Persist watched projects
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    List<String> d = Lists.newArrayList(projectName2);
+    gApi.accounts().self().deleteWatchedProjects(d);
+    projectsToWatch.remove(pwi);
+
+    List<ProjectWatchInfo> persistedWatchedProjects =
+        gApi.accounts().self().getWatchedProjects();
+
+    assertThat(persistedWatchedProjects).doesNotContain(pwi);
+    assertThat(persistedWatchedProjects).containsAllIn(projectsToWatch);
+  }
+
+  @Test
+  public void watchNonExistingProject() throws Exception {
+    String projectName = NEW_PROJECT_NAME + "3";
+
+    List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(2);
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    exception.expect(UnprocessableEntityException.class);
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+  }
+
+  @Test
+  public void deleteNonExistingProject() throws Exception {
+    String projectName = project.get();
+
+    // Let another user watch a project
+    setApiUser(admin);
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    // Try to delete a watched project using a different user
+    List<String> d = Lists.newArrayList(projectName);
+    gApi.accounts().self().deleteWatchedProjects(d);
+
+    setApiUser(user);
+    exception.expect(UnprocessableEntityException.class);
+    gApi.accounts().self().deleteWatchedProjects(d);
+  }
+
+  @Test
+  public void modifyProjectWatchUsingOmittedValues() throws Exception {
+    String projectName = project.get();
+
+    // Let another user watch a project
+    setApiUser(admin);
+    List<ProjectWatchInfo> projectsToWatch = new LinkedList<>();
+
+    ProjectWatchInfo pwi = new ProjectWatchInfo();
+    pwi.project = projectName;
+    pwi.notifyAbandonedChanges = true;
+    pwi.notifyNewChanges = true;
+    pwi.notifyAllComments = true;
+    projectsToWatch.add(pwi);
+
+    // Persist a defined state
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    // Omit previously set value - will set it to false on the server
+    // The response will not carry this field then as we omit sending
+    // false values in JSON
+    pwi.notifyNewChanges = null;
+
+    // Perform update
+    gApi.accounts().self().setWatchedProjects(projectsToWatch);
+
+    List<ProjectWatchInfo> watchedProjects =
+        gApi.accounts().self().getWatchedProjects();
+
+    assertThat(watchedProjects).containsAllIn(projectsToWatch);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index a6e54b2..2105b8c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
@@ -43,6 +44,12 @@
   EditPreferencesInfo setEditPreferences(EditPreferencesInfo in)
       throws RestApiException;
 
+  List<ProjectWatchInfo> getWatchedProjects() throws RestApiException;
+  List<ProjectWatchInfo> setWatchedProjects(List<ProjectWatchInfo> in)
+      throws RestApiException;
+  void deleteWatchedProjects(List<String> in)
+      throws RestApiException;
+
   void starChange(String id) throws RestApiException;
   void unstarChange(String id) throws RestApiException;
   void addEmail(EmailInput input) throws RestApiException;
@@ -105,6 +112,24 @@
     }
 
     @Override
+    public List<ProjectWatchInfo> getWatchedProjects()
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<ProjectWatchInfo> setWatchedProjects(
+        List<ProjectWatchInfo> in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void deleteWatchedProjects(List<String> in)
+        throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void starChange(String id) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
new file mode 100644
index 0000000..beb869e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.client;
+
+import java.util.Objects;
+
+public class ProjectWatchInfo {
+  public String project;
+  public String filter;
+
+  public Boolean notifyNewChanges;
+  public Boolean notifyNewPatchSets;
+  public Boolean notifyAllComments;
+  public Boolean notifySubmittedChanges;
+  public Boolean notifyAbandonedChanges;
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof ProjectWatchInfo) {
+      ProjectWatchInfo w = (ProjectWatchInfo) obj;
+      return Objects.equals(project, w.project)
+          && Objects.equals(filter, w.filter)
+          && Objects.equals(notifyNewChanges, w.notifyNewChanges)
+          && Objects.equals(notifyNewPatchSets, w.notifyNewPatchSets)
+          && Objects.equals(notifyAllComments, w.notifyAllComments)
+          && Objects.equals(notifySubmittedChanges, w.notifySubmittedChanges)
+          && Objects.equals(notifyAbandonedChanges, w.notifyAbandonedChanges);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects
+        .hash(project, filter, notifyNewChanges, notifyNewPatchSets,
+            notifyAllComments, notifySubmittedChanges, notifyAbandonedChanges);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
new file mode 100644
index 0000000..674cba6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+@Singleton
+public class DeleteWatchedProjects
+    implements RestModifyView<AccountResource, List<String>> {
+
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<IdentifiedUser> self;
+
+  @Inject
+  DeleteWatchedProjects(Provider<ReviewDb> dbProvider,
+      Provider<IdentifiedUser> self) {
+    this.dbProvider = dbProvider;
+    this.self = self;
+  }
+
+  @Override
+  public Response<?> apply(
+      AccountResource rsrc, List<String> input)
+      throws UnprocessableEntityException, OrmException, AuthException {
+    if (self.get() != rsrc.getUser()) {
+      throw new AuthException("It is not allowed to edit project watches "
+          + "of other users");
+    }
+    ResultSet<AccountProjectWatch> watchedProjects =
+        dbProvider.get().accountProjectWatches()
+            .byAccount(rsrc.getUser().getAccountId());
+    HashMap<String, AccountProjectWatch> watchedProjectsMap = new HashMap<>();
+    for (AccountProjectWatch watchedProject : watchedProjects) {
+      watchedProjectsMap
+          .put(watchedProject.getProjectNameKey().get(), watchedProject);
+    }
+
+    if (input != null) {
+      List<AccountProjectWatch.Key> keysToDelete = new LinkedList<>();
+      for (String projectKeyToDelete : input) {
+        if (!watchedProjectsMap.containsKey(projectKeyToDelete))
+          throw new UnprocessableEntityException(projectKeyToDelete
+              + " is not currently watched by this user.");
+        keysToDelete.add(watchedProjectsMap.get(projectKeyToDelete).getKey());
+      }
+      dbProvider.get().accountProjectWatches().deleteKeys(keysToDelete);
+    }
+
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
new file mode 100644
index 0000000..fd00b7a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.LinkedList;
+import java.util.List;
+
+@Singleton
+public class GetWatchedProjects implements RestReadView<AccountResource> {
+
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<IdentifiedUser> self;
+
+  @Inject
+  public GetWatchedProjects(Provider<ReviewDb> dbProvider,
+      Provider<IdentifiedUser> self) {
+    this.dbProvider = dbProvider;
+    this.self = self;
+  }
+
+  @Override
+  public List<ProjectWatchInfo> apply(AccountResource rsrc)
+      throws OrmException, AuthException {
+    if (self.get() != rsrc.getUser()) {
+      throw new AuthException("It is not allowed to list project watches "
+          + "of other users");
+    }
+    List<ProjectWatchInfo> projectWatchInfos = new LinkedList<>();
+    Iterable<AccountProjectWatch> projectWatches =
+        dbProvider.get().accountProjectWatches()
+            .byAccount(rsrc.getUser().getAccountId());
+    for (AccountProjectWatch a : projectWatches) {
+      ProjectWatchInfo pwi = new ProjectWatchInfo();
+      pwi.filter = a.getFilter();
+      pwi.project = a.getProjectNameKey().get();
+      pwi.notifyAbandonedChanges =
+          toBoolean(
+              a.isNotify(AccountProjectWatch.NotifyType.ABANDONED_CHANGES));
+      pwi.notifyNewChanges =
+          toBoolean(a.isNotify(AccountProjectWatch.NotifyType.NEW_CHANGES));
+      pwi.notifyNewPatchSets =
+          toBoolean(a.isNotify(AccountProjectWatch.NotifyType.NEW_PATCHSETS));
+      pwi.notifySubmittedChanges =
+          toBoolean(
+              a.isNotify(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES));
+      pwi.notifyAllComments =
+          toBoolean(a.isNotify(AccountProjectWatch.NotifyType.ALL_COMMENTS));
+      projectWatchInfos.add(pwi);
+    }
+    return projectWatchInfos;
+  }
+
+  private static Boolean toBoolean(boolean value) {
+    return value ? true : null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index 54d4cc0..b11be39 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -56,6 +56,10 @@
     delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
     child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
     post(ACCOUNT_KIND, "sshkeys").to(AddSshKey.class);
+    get(ACCOUNT_KIND, "watched.projects").to(GetWatchedProjects.class);
+    post(ACCOUNT_KIND, "watched.projects").to(PostWatchedProjects.class);
+    post(ACCOUNT_KIND, "watched.projects:delete")
+        .to(DeleteWatchedProjects.class);
 
     get(SSH_KEY_KIND).to(GetSshKey.class);
     delete(SSH_KEY_KIND).to(DeleteSshKey.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java
new file mode 100644
index 0000000..cf67ad9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectsCollection;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+@Singleton
+public class PostWatchedProjects
+    implements RestModifyView<AccountResource, List<ProjectWatchInfo>> {
+  private final Provider<IdentifiedUser> self;
+  private GetWatchedProjects getWatchedProjects;
+  private Provider<ReviewDb> dbProvider;
+  private ProjectsCollection projectsCollection;
+
+  @Inject
+  public PostWatchedProjects(GetWatchedProjects getWatchedProjects,
+      Provider<ReviewDb> dbProvider,
+      ProjectsCollection projectsCollection,
+      Provider<IdentifiedUser> self) {
+    this.getWatchedProjects = getWatchedProjects;
+    this.dbProvider = dbProvider;
+    this.projectsCollection = projectsCollection;
+    this.self = self;
+  }
+
+  @Override
+  public List<ProjectWatchInfo> apply(AccountResource rsrc,
+      List<ProjectWatchInfo> input)
+      throws OrmException, RestApiException, IOException {
+    if (self.get() != rsrc.getUser()) {
+      throw new AuthException("not allowed to edit project watches");
+    }
+    List<AccountProjectWatch> accountProjectWatchList =
+        getAccountProjectWatchList(input, rsrc.getUser().getAccountId());
+    dbProvider.get().accountProjectWatches().upsert(accountProjectWatchList);
+    return getWatchedProjects.apply(rsrc);
+  }
+
+  private List<AccountProjectWatch> getAccountProjectWatchList(
+      List<ProjectWatchInfo> input, Account.Id accountId)
+      throws UnprocessableEntityException, BadRequestException, IOException {
+    List<AccountProjectWatch> watchedProjects = new LinkedList<>();
+    for (ProjectWatchInfo a : input) {
+      if (a.project == null) {
+        throw new BadRequestException("project name must be specified");
+      }
+
+      Project.NameKey projectKey =
+          projectsCollection.parse(a.project).getNameKey();
+
+      AccountProjectWatch.Key key =
+          new AccountProjectWatch.Key(accountId, projectKey, a.filter);
+      AccountProjectWatch apw = new AccountProjectWatch(key);
+      apw.setNotify(AccountProjectWatch.NotifyType.ABANDONED_CHANGES,
+          toBoolean(a.notifyAbandonedChanges));
+      apw.setNotify(AccountProjectWatch.NotifyType.ALL_COMMENTS,
+          toBoolean(a.notifyAllComments));
+      apw.setNotify(AccountProjectWatch.NotifyType.NEW_CHANGES,
+          toBoolean(a.notifyNewChanges));
+      apw.setNotify(AccountProjectWatch.NotifyType.NEW_PATCHSETS,
+          toBoolean(a.notifyNewPatchSets));
+      apw.setNotify(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES,
+          toBoolean(a.notifySubmittedChanges));
+      watchedProjects.add(apw);
+    }
+    return watchedProjects;
+  }
+
+  private boolean toBoolean(Boolean b) {
+    return b == null ? false : b;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 709020f..5860391 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
@@ -39,10 +40,13 @@
 import com.google.gerrit.server.account.GetEditPreferences;
 import com.google.gerrit.server.account.GetPreferences;
 import com.google.gerrit.server.account.GetSshKeys;
+import com.google.gerrit.server.account.GetWatchedProjects;
+import com.google.gerrit.server.account.DeleteWatchedProjects;
 import com.google.gerrit.server.account.SetDiffPreferences;
 import com.google.gerrit.server.account.SetEditPreferences;
 import com.google.gerrit.server.account.SetPreferences;
 import com.google.gerrit.server.account.SshKeys;
+import com.google.gerrit.server.account.PostWatchedProjects;
 import com.google.gerrit.server.account.StarredChanges;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
@@ -71,6 +75,9 @@
   private final SetDiffPreferences setDiffPreferences;
   private final GetEditPreferences getEditPreferences;
   private final SetEditPreferences setEditPreferences;
+  private final GetWatchedProjects getWatchedProjects;
+  private final PostWatchedProjects postWatchedProjects;
+  private final DeleteWatchedProjects deleteWatchedProjects;
   private final StarredChanges.Create starredChangesCreate;
   private final StarredChanges.Delete starredChangesDelete;
   private final CreateEmail.Factory createEmailFactory;
@@ -90,6 +97,9 @@
       SetDiffPreferences setDiffPreferences,
       GetEditPreferences getEditPreferences,
       SetEditPreferences setEditPreferences,
+      GetWatchedProjects getWatchedProjects,
+      PostWatchedProjects postWatchedProjects,
+      DeleteWatchedProjects deleteWatchedProjects,
       StarredChanges.Create starredChangesCreate,
       StarredChanges.Delete starredChangesDelete,
       CreateEmail.Factory createEmailFactory,
@@ -109,6 +119,9 @@
     this.setDiffPreferences = setDiffPreferences;
     this.getEditPreferences = getEditPreferences;
     this.setEditPreferences = setEditPreferences;
+    this.getWatchedProjects = getWatchedProjects;
+    this.postWatchedProjects = postWatchedProjects;
+    this.deleteWatchedProjects = deleteWatchedProjects;
     this.starredChangesCreate = starredChangesCreate;
     this.starredChangesDelete = starredChangesDelete;
     this.createEmailFactory = createEmailFactory;
@@ -192,6 +205,35 @@
   }
 
   @Override
+  public List<ProjectWatchInfo> getWatchedProjects() throws RestApiException {
+    try {
+      return getWatchedProjects.apply(account);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get watched projects", e);
+    }
+  }
+
+  @Override
+  public List<ProjectWatchInfo> setWatchedProjects(
+      List<ProjectWatchInfo> in) throws RestApiException {
+    try {
+      return postWatchedProjects.apply(account, in);
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot update watched projects", e);
+    }
+  }
+
+  @Override
+  public void deleteWatchedProjects(List<String> in)
+      throws RestApiException {
+    try {
+      deleteWatchedProjects.apply(account, in);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot delete watched projects", e);
+    }
+  }
+
+  @Override
   public void starChange(String id) throws RestApiException {
     try {
       ChangeResource rsrc = changes.parse(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index 0482034..1f8930c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -49,7 +49,7 @@
  * a transaction of a submission, this caches open repositories to satisfy
  * that requirement.
  */
-class MergeOpRepoManager implements AutoCloseable {
+public class MergeOpRepoManager implements AutoCloseable {
   public class OpenRepo {
     final Repository repo;
     final CodeReviewRevWalk rw;