Fix(watched-projects): Surface existing project errors

Adding a new watched project could fail because, during the add
operation, an access check is performed on the combined list of watched
projects (including the newly added one and existing ones), and this
check can fail if the existing list contains inaccessible projects. This
can happen when a user adds a project to their watched list, and that
project is later made hidden or deleted.

This change surfaces these errors when fetching the watched projects
list. By displaying these errors in the UI, users can identify and
remove the inaccessible projects, resolving the issue and allowing them
to successfully add new watched projects.

Change-Id: I6e55fd3fabc6de315a0e9fbfb280e2e5839c5688
Release-Notes: skip
Google-Bug-Id: b/389630826
Signed-off-by: Milutin Kristofic <milutin@google.com>
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index cb3517d..1c77e4a 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -3009,6 +3009,7 @@
 |Field Name                 |        |Description
 |`project`                  |        |The name of the project.
 |`filter`                   |optional|A filter string to be applied to the project.
+|`problem`                  |optional|An error message when project is for example hidden or deleted.
 |`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.
diff --git a/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java b/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
index 8f5af76..c446d87 100644
--- a/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
+++ b/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
@@ -19,6 +19,7 @@
 public class ProjectWatchInfo {
   public String project;
   public String filter;
+  public String problem;
 
   public Boolean notifyNewChanges;
   public Boolean notifyNewPatchSets;
@@ -32,6 +33,7 @@
       ProjectWatchInfo w = (ProjectWatchInfo) obj;
       return Objects.equals(project, w.project)
           && Objects.equals(filter, w.filter)
+          && Objects.equals(problem, w.problem)
           && Objects.equals(notifyNewChanges, w.notifyNewChanges)
           && Objects.equals(notifyNewPatchSets, w.notifyNewPatchSets)
           && Objects.equals(notifyAllComments, w.notifyAllComments)
@@ -46,6 +48,7 @@
     return Objects.hash(
         project,
         filter,
+        problem,
         notifyNewChanges,
         notifyNewPatchSets,
         notifyAllComments,
@@ -61,6 +64,9 @@
     if (filter != null) {
       b.append("%filter=").append(filter);
     }
+    if (problem != null) {
+      b.append("%problem=").append(problem);
+    }
     b.append("(notifyAbandonedChanges=")
         .append(toBoolean(notifyAbandonedChanges))
         .append(", notifyAllComments=")
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index 00c70d0..8f047a3 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
@@ -35,6 +36,7 @@
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -53,13 +55,18 @@
   private final PermissionBackend permissionBackend;
   private final Provider<IdentifiedUser> self;
   private final Accounts accounts;
+  private final ProjectsCollection projectsCollection;
 
   @Inject
   public GetWatchedProjects(
-      PermissionBackend permissionBackend, Provider<IdentifiedUser> self, Accounts accounts) {
+      PermissionBackend permissionBackend,
+      Provider<IdentifiedUser> self,
+      Accounts accounts,
+      ProjectsCollection projectsCollection) {
     this.permissionBackend = permissionBackend;
     this.self = self;
     this.accounts = accounts;
+    this.projectsCollection = projectsCollection;
   }
 
   @Override
@@ -84,11 +91,16 @@
             .collect(toList()));
   }
 
-  private static ProjectWatchInfo toProjectWatchInfo(
+  private ProjectWatchInfo toProjectWatchInfo(
       ProjectWatchKey key, ImmutableSet<NotifyType> watchTypes) {
     ProjectWatchInfo pwi = new ProjectWatchInfo();
     pwi.filter = key.filter();
     pwi.project = key.project().get();
+    try {
+      var unused = projectsCollection.parse(key.project().get());
+    } catch (RestApiException | IOException | PermissionBackendException e) {
+      pwi.problem = e.getMessage();
+    }
     pwi.notifyAbandonedChanges = toBoolean(watchTypes.contains(NotifyType.ABANDONED_CHANGES));
     pwi.notifyNewChanges = toBoolean(watchTypes.contains(NotifyType.NEW_CHANGES));
     pwi.notifyNewPatchSets = toBoolean(watchTypes.contains(NotifyType.NEW_PATCHSETS));