Do not ignore commit in a group's history if the author is not parsed.

Some old versions of gerrit created commits in refs/groups/* using
server identity (e.g. "Gerrit Code Review <server@googlesource.com>).
Such commits contain a valid data which should be parsed and returned
by the groups audit log endpoint.

The options to keep the old behaviour is added in case some users wants
it.

Google-Bug-Id: b/218652717
Release-Notes: Groups Audit Log API - Gerrit now parses all commits in refs/groups/... branch.
Change-Id: I7eeb9537dd71e9ba5e65a696cf43d000f123d21c
(cherry picked from commit 6576d2c6947a1e044b7bd9a80d88e2925b1cfcdb)
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 2bfaa40..52d9da0 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2826,6 +2826,21 @@
 [[groups]]
 === Section groups
 
+[[groups.auditLog.ignoreRecordsFromUnidentifiedUsers]]groups.auditLog.ignoreRecordsFromUnidentifiedUsers::
++
+Controls whether AuditLogReader should ignore commits created by unidentified users.
+If true, then AuditLogReader ignores commits in the refs/groups/* made by unidentified users (i.e.
+when the author of a commit can't be parsed as account id).
++
+The current version of Gerrit writes identified users as authors for new refs/groups/* commits.
+However, some old versions used a server identity as the author (e.g. "Gerrit Code Review
+<server@googlesource.com>") for such commits. Such string can't be converted to account id but
+usually the commit shouldn't be ignored.
++
+By default, false.
++
+Setting it to true may lead to some unexpected results in audit log and must be set carefully.
+
 [[groups.includeExternalUsersInRegisteredUsersGroup]]groups.includeExternalUsersInRegisteredUsersGroup::
 +
 Controls whether external users (these are users we have sufficient
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
index 8da2f50..ae2b2fc 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogReader.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -36,6 +36,7 @@
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -53,10 +54,14 @@
   private final AllUsersName allUsersName;
   private final NoteDbUtil noteDbUtil;
 
+  private final boolean ignoreRecordsFromUnidentifiedUsers;
+
   @Inject
-  public AuditLogReader(AllUsersName allUsersName, NoteDbUtil noteDbUtil) {
+  public AuditLogReader(AllUsersName allUsersName, NoteDbUtil noteDbUtil, Config cfg) {
     this.allUsersName = allUsersName;
     this.noteDbUtil = noteDbUtil;
+    ignoreRecordsFromUnidentifiedUsers =
+        cfg.getBoolean("groups", "auditLog", "ignoreRecordsFromUnidentifiedUsers", false);
   }
 
   // Having separate methods for reading the two types of audit records mirrors the split in
@@ -144,9 +149,12 @@
   private Optional<ParsedCommit> parse(AccountGroup.UUID uuid, RevCommit c) {
     Optional<Account.Id> authorId = noteDbUtil.parseIdent(c.getAuthorIdent());
     if (!authorId.isPresent()) {
-      // Only report audit events from identified users, since this was a non-nullable field in
-      // ReviewDb. May be revisited.
-      return Optional.empty();
+      if (ignoreRecordsFromUnidentifiedUsers) {
+        // Only report audit events from identified users, since this was a non-nullable field in
+        // ReviewDb.
+        return Optional.empty();
+      }
+      authorId = Optional.of(Account.UNKNOWN_ACCOUNT_ID);
     }
 
     List<Account.Id> addedMembers = new ArrayList<>();
diff --git a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
index 5d9b15d..c4cc878 100644
--- a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
@@ -28,7 +28,9 @@
 import com.google.gerrit.server.account.externalids.storage.notedb.DisabledExternalIdCache;
 import com.google.gerrit.server.notedb.NoteDbUtil;
 import java.time.Instant;
+import java.util.Optional;
 import java.util.Set;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Before;
 import org.junit.Test;
@@ -41,7 +43,8 @@
   @Before
   public void setUp() throws Exception {
     auditLogReader =
-        new AuditLogReader(allUsersName, new NoteDbUtil(SERVER_ID, new DisabledExternalIdCache()));
+        new AuditLogReader(
+            allUsersName, new NoteDbUtil(SERVER_ID, new DisabledExternalIdCache()), new Config());
   }
 
   @Test
@@ -89,6 +92,67 @@
   }
 
   @Test
+  public void addMemberByUnknownAuthorAndRemoveMemberByKnownAuthor() throws Exception {
+    InternalGroup group = createGroupAsUser(1, "test-group");
+    AccountGroup.UUID uuid = group.getGroupUUID();
+    AccountGroupMemberAudit expAudit1 =
+        createExpMemberAudit(group.getId(), userId, userId, getTipTimestamp(uuid));
+
+    // An unidentified user adds account 100002 to the group.
+    Account.Id id = Account.id(100002);
+    addMembers(
+        uuid,
+        ImmutableSet.of(id),
+        new PersonIdent("Test ident", "random@gerrit"),
+        Optional.of(Instant.ofEpochSecond(1)));
+    // Identified user removes account 100002 from the group.
+    removeMembers(uuid, ImmutableSet.of(id));
+
+    AccountGroupMemberAudit expAudit2 =
+        createExpMemberAudit(
+                group.getId(), id, Account.UNKNOWN_ACCOUNT_ID, Instant.ofEpochSecond(1))
+            .toBuilder()
+            .removed(userId, getTipTimestamp(uuid))
+            .build();
+    assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid))
+        .containsExactly(expAudit1, expAudit2)
+        .inOrder();
+  }
+
+  @Test
+  public void addMemberByUnknownAuthorAndRemoveMemberByKnownAuthor_ignoreUnidentifiedUser()
+      throws Exception {
+    // This test doesn't repeat the previous test, because it doesn't produce the correct result.
+    // Instead this test adds and removes uses by an unidentified user and expects nothing in the
+    // output.
+    Config cfg = new Config();
+    cfg.setBoolean("groups", "auditLog", "ignoreRecordsFromUnidentifiedUsers", true);
+    auditLogReader =
+        new AuditLogReader(
+            allUsersName, new NoteDbUtil(SERVER_ID, new DisabledExternalIdCache()), cfg);
+    InternalGroup group = createGroupAsUser(1, "test-group");
+    AccountGroup.UUID uuid = group.getGroupUUID();
+    AccountGroupMemberAudit expAudit1 =
+        createExpMemberAudit(group.getId(), userId, userId, getTipTimestamp(uuid));
+
+    // An unidentified user adds account 100002 to the group.
+    Account.Id id = Account.id(100002);
+    addMembers(
+        uuid,
+        ImmutableSet.of(id),
+        new PersonIdent("Test ident", "random@gerrit"),
+        Optional.of(Instant.ofEpochSecond(1)));
+    // Identified user removes account 100002 from the group.
+    removeMembers(
+        uuid,
+        ImmutableSet.of(id),
+        new PersonIdent("Test ident", "random@gerrit"),
+        Optional.of(Instant.ofEpochSecond(100)));
+
+    assertThat(auditLogReader.getMembersAudit(allUsersRepo, uuid)).containsExactly(expAudit1);
+  }
+
+  @Test
   public void addMultiMembers() throws Exception {
     InternalGroup group = createGroupAsUser(1, "test-group");
     AccountGroup.Id groupId = group.getId();
@@ -268,26 +332,53 @@
   }
 
   private void updateGroup(AccountGroup.UUID uuid, GroupDelta groupDelta) throws Exception {
+    updateGroup(uuid, groupDelta, userIdent);
+  }
+
+  private void updateGroup(AccountGroup.UUID uuid, GroupDelta groupDelta, PersonIdent authorIdent)
+      throws Exception {
     testRefAction(
         () -> {
           GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersName, allUsersRepo, uuid);
           groupConfig.setGroupDelta(groupDelta, getAuditLogFormatter());
-          groupConfig.commit(createMetaDataUpdate(userIdent));
+          groupConfig.commit(createMetaDataUpdate(authorIdent));
         });
   }
 
   private void addMembers(AccountGroup.UUID groupUuid, Set<Account.Id> ids) throws Exception {
-    GroupDelta groupDelta =
-        GroupDelta.builder().setMemberModification(memberIds -> Sets.union(memberIds, ids)).build();
-    updateGroup(groupUuid, groupDelta);
+    addMembers(groupUuid, ids, userIdent, Optional.empty());
+  }
+
+  private void addMembers(
+      AccountGroup.UUID groupUuid,
+      Set<Account.Id> ids,
+      PersonIdent authorIdent,
+      Optional<Instant> updatedOn)
+      throws Exception {
+    GroupDelta.Builder groupDelta =
+        GroupDelta.builder().setMemberModification(memberIds -> Sets.union(memberIds, ids));
+    if (updatedOn.isPresent()) {
+      groupDelta.setUpdatedOn(updatedOn.get());
+    }
+    updateGroup(groupUuid, groupDelta.build(), authorIdent);
   }
 
   private void removeMembers(AccountGroup.UUID groupUuid, Set<Account.Id> ids) throws Exception {
-    GroupDelta groupDelta =
-        GroupDelta.builder()
-            .setMemberModification(memberIds -> Sets.difference(memberIds, ids))
-            .build();
-    updateGroup(groupUuid, groupDelta);
+    removeMembers(groupUuid, ids, userIdent, Optional.empty());
+  }
+
+  private void removeMembers(
+      AccountGroup.UUID groupUuid,
+      Set<Account.Id> ids,
+      PersonIdent authorIdent,
+      Optional<Instant> updatedOn)
+      throws Exception {
+    GroupDelta.Builder groupDelta =
+        GroupDelta.builder().setMemberModification(memberIds -> Sets.difference(memberIds, ids));
+    if (updatedOn.isPresent()) {
+      groupDelta.setUpdatedOn(updatedOn.get());
+    }
+    updateGroup(groupUuid, groupDelta.build(), authorIdent);
   }
 
   private void addSubgroups(AccountGroup.UUID groupUuid, Set<AccountGroup.UUID> uuids)