Merge "docs/config-themes: clarify and link to JavaScript plugin dev"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 33e7804..58a3724 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6520,7 +6520,14 @@
 |`topic`              |optional|The topic to which this change belongs.
 |`attention_set`      |optional|
 The map that maps link:rest-api-accounts.html#account-id[account IDs]
-to link:#attention-set-info[AttentionSetInfo] of that account.
+to link:#attention-set-info[AttentionSetInfo] of that account. Those are all
+accounts that are currently in the attention set.
+|`removed_from_attention_set`      |optional|
+The map that maps link:rest-api-accounts.html#account-id[account IDs]
+to link:#attention-set-info[AttentionSetInfo] of that account. Those are all
+accounts that were in the attention set but were removed. The
+link:#attention-set-info[AttentionSetInfo] is the latest and most recent removal
+ of the account from the attention set.
 |`assignee`           |optional|
 The assignee of the change as an link:rest-api-accounts.html#account-info[
 AccountInfo] entity.
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 2bb3dd7..55e4670 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -46,6 +46,8 @@
    */
   public Map<Integer, AttentionSetInfo> attentionSet;
 
+  public Map<Integer, AttentionSetInfo> removedFromAttentionSet;
+
   public AccountInfo assignee;
   public Collection<String> hashtags;
   public String changeId;
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index 54ebf40..ffbd30b 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -118,6 +118,10 @@
     copy.topic = changeInfo.topic;
     copy.attentionSet =
         changeInfo.attentionSet == null ? null : ImmutableMap.copyOf(changeInfo.attentionSet);
+    copy.removedFromAttentionSet =
+        changeInfo.removedFromAttentionSet == null
+            ? null
+            : ImmutableMap.copyOf(changeInfo.removedFromAttentionSet);
     copy.assignee = changeInfo.assignee;
     copy.hashtags = changeInfo.hashtags;
     copy.changeId = changeInfo.changeId;
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 328c5de..e57238b 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -35,6 +35,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
 import static com.google.gerrit.server.ChangeMessagesUtil.createChangeMessageInfo;
 import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
+import static com.google.gerrit.server.util.AttentionSetUtil.removalsOnly;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Joiner;
@@ -603,6 +604,12 @@
     out.branch = in.getDest().shortName();
     out.topic = in.getTopic();
     if (!cd.attentionSet().isEmpty()) {
+      out.removedFromAttentionSet =
+          removalsOnly(cd.attentionSet()).stream()
+              .collect(
+                  toImmutableMap(
+                      a -> a.account().get(),
+                      a -> AttentionSetUtil.createAttentionSetInfo(a, accountLoader)));
       out.attentionSet =
           // This filtering should match GetAttentionSet.
           additionsOnly(cd.attentionSet()).stream()
diff --git a/java/com/google/gerrit/server/util/AttentionSetUtil.java b/java/com/google/gerrit/server/util/AttentionSetUtil.java
index 9238b44..98200fd 100644
--- a/java/com/google/gerrit/server/util/AttentionSetUtil.java
+++ b/java/com/google/gerrit/server/util/AttentionSetUtil.java
@@ -43,6 +43,14 @@
         .collect(ImmutableSet.toImmutableSet());
   }
 
+  /** Returns only updates where the user was removed. */
+  public static ImmutableSet<AttentionSetUpdate> removalsOnly(
+      Collection<AttentionSetUpdate> updates) {
+    return updates.stream()
+        .filter(u -> u.operation() == Operation.REMOVE)
+        .collect(ImmutableSet.toImmutableSet());
+  }
+
   /**
    * Validates the input for AttentionSetInput. This must be called for all inputs that relate to
    * adding or removing attention set entries, except for {@link
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 0a11b15..4bce5d8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -261,6 +261,12 @@
             fakeClock.now(), user.id(), AttentionSetUpdate.Operation.REMOVE, "removed");
     assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
 
+    // The removal also shows up in AttentionSetInfo.
+    AttentionSetInfo attentionSetInfo =
+        Iterables.getOnlyElement(change(r).get().removedFromAttentionSet.values());
+    assertThat(attentionSetInfo.reason).isEqualTo("removed");
+    assertThat(attentionSetInfo.account).isEqualTo(getAccountInfo(user.id()));
+
     // Second removal is ignored.
     fakeClock.advance(Duration.ofSeconds(42));
     change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed again"));