LCOV - code coverage report
Current view: top level - server/group/db - GroupNameNotes.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 144 150 96.0 %
Date: 2022-11-19 15:00:39 Functions: 20 20 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2017 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.group.db;
      16             : 
      17             : import static com.google.common.collect.ImmutableBiMap.toImmutableBiMap;
      18             : import static java.nio.charset.StandardCharsets.UTF_8;
      19             : import static java.util.Objects.requireNonNull;
      20             : import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
      21             : 
      22             : import com.google.common.annotations.VisibleForTesting;
      23             : import com.google.common.base.Strings;
      24             : import com.google.common.collect.HashMultiset;
      25             : import com.google.common.collect.ImmutableBiMap;
      26             : import com.google.common.collect.ImmutableList;
      27             : import com.google.common.collect.Multiset;
      28             : import com.google.common.flogger.FluentLogger;
      29             : import com.google.common.hash.Hashing;
      30             : import com.google.gerrit.common.Nullable;
      31             : import com.google.gerrit.entities.AccountGroup;
      32             : import com.google.gerrit.entities.GroupReference;
      33             : import com.google.gerrit.entities.Project;
      34             : import com.google.gerrit.entities.RefNames;
      35             : import com.google.gerrit.exceptions.DuplicateKeyException;
      36             : import com.google.gerrit.git.ObjectIds;
      37             : import com.google.gerrit.server.git.meta.VersionedMetaData;
      38             : import com.google.gerrit.server.logging.Metadata;
      39             : import com.google.gerrit.server.logging.TraceContext;
      40             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      41             : import java.io.IOException;
      42             : import java.util.Collection;
      43             : import java.util.Map;
      44             : import java.util.Objects;
      45             : import java.util.Optional;
      46             : import org.eclipse.jgit.errors.ConfigInvalidException;
      47             : import org.eclipse.jgit.lib.BatchRefUpdate;
      48             : import org.eclipse.jgit.lib.CommitBuilder;
      49             : import org.eclipse.jgit.lib.Config;
      50             : import org.eclipse.jgit.lib.ObjectId;
      51             : import org.eclipse.jgit.lib.ObjectInserter;
      52             : import org.eclipse.jgit.lib.ObjectReader;
      53             : import org.eclipse.jgit.lib.PersonIdent;
      54             : import org.eclipse.jgit.lib.Ref;
      55             : import org.eclipse.jgit.lib.Repository;
      56             : import org.eclipse.jgit.notes.Note;
      57             : import org.eclipse.jgit.notes.NoteMap;
      58             : import org.eclipse.jgit.revwalk.RevCommit;
      59             : import org.eclipse.jgit.revwalk.RevWalk;
      60             : import org.eclipse.jgit.transport.ReceiveCommand;
      61             : 
      62             : /**
      63             :  * An enforcer of unique names for groups in NoteDb.
      64             :  *
      65             :  * <p>The way groups are stored in NoteDb (see {@link GroupConfig}) doesn't enforce unique names,
      66             :  * even though groups in Gerrit must not have duplicate names. The storage format doesn't allow to
      67             :  * quickly look up whether a name has already been used either. That's why we additionally keep a
      68             :  * map of name/UUID pairs and manage it with this class.
      69             :  *
      70             :  * <p>To claim the name for a new group, create an instance of {@code GroupNameNotes} via {@link
      71             :  * #forNewGroup(Project.NameKey, Repository, AccountGroup.UUID, AccountGroup.NameKey)} and call
      72             :  * {@link #commit(com.google.gerrit.server.git.meta.MetaDataUpdate) commit(MetaDataUpdate)} on it.
      73             :  * For renaming, call {@link #forRename(Project.NameKey, Repository, AccountGroup.UUID,
      74             :  * AccountGroup.NameKey, AccountGroup.NameKey)} and also commit the returned {@code GroupNameNotes}.
      75             :  * Both times, the creation of the {@code GroupNameNotes} will fail if the (new) name is already
      76             :  * used. Committing the {@code GroupNameNotes} is necessary to make the adjustments for real.
      77             :  *
      78             :  * <p>The map has an additional benefit: We can quickly iterate over all group name/UUID pairs
      79             :  * without having to load all groups completely (which is costly).
      80             :  *
      81             :  * <p><em>Internal details</em>
      82             :  *
      83             :  * <p>The map of names is represented by Git {@link Note notes}. They are stored on the branch
      84             :  * {@link RefNames#REFS_GROUPNAMES}. Each commit on the branch reflects one moment in time of the
      85             :  * complete map.
      86             :  *
      87             :  * <p>As key for the notes, we use the SHA-1 of the name. As data, they contain a text version of a
      88             :  * JGit {@link Config} file. That config file has two entries:
      89             :  *
      90             :  * <ul>
      91             :  *   <li>the name of the group (as clear text)
      92             :  *   <li>the UUID of the group which currently has this name
      93             :  * </ul>
      94             :  */
      95             : public class GroupNameNotes extends VersionedMetaData {
      96         152 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      97             : 
      98             :   private static final String SECTION_NAME = "group";
      99             :   private static final String UUID_PARAM = "uuid";
     100             :   private static final String NAME_PARAM = "name";
     101             : 
     102             :   @VisibleForTesting
     103             :   static final String UNIQUE_REF_ERROR = "GroupReference collection must contain unique references";
     104             : 
     105             :   /**
     106             :    * Creates an instance of {@code GroupNameNotes} for use when renaming a group.
     107             :    *
     108             :    * <p><strong>Note: </strong>The returned instance of {@code GroupNameNotes} has to be committed
     109             :    * via {@link #commit(com.google.gerrit.server.git.meta.MetaDataUpdate) commit(MetaDataUpdate)} in
     110             :    * order to claim the new name and free up the old one.
     111             :    *
     112             :    * @param projectName the name of the project which holds the commits of the notes
     113             :    * @param repository the repository which holds the commits of the notes
     114             :    * @param groupUuid the UUID of the group which is renamed
     115             :    * @param oldName the current name of the group
     116             :    * @param newName the new name of the group
     117             :    * @return an instance of {@code GroupNameNotes} configured for a specific renaming of a group
     118             :    * @throws IOException if the repository can't be accessed for some reason
     119             :    * @throws ConfigInvalidException if the note for the specified group doesn't exist or is in an
     120             :    *     invalid state
     121             :    * @throws DuplicateKeyException if a group with the new name already exists
     122             :    */
     123             :   public static GroupNameNotes forRename(
     124             :       Project.NameKey projectName,
     125             :       Repository repository,
     126             :       AccountGroup.UUID groupUuid,
     127             :       AccountGroup.NameKey oldName,
     128             :       AccountGroup.NameKey newName)
     129             :       throws IOException, ConfigInvalidException, DuplicateKeyException {
     130           4 :     requireNonNull(oldName);
     131           4 :     requireNonNull(newName);
     132             : 
     133           4 :     GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, oldName, newName);
     134           4 :     groupNameNotes.load(projectName, repository);
     135           4 :     groupNameNotes.ensureNewNameIsNotUsed();
     136           4 :     return groupNameNotes;
     137             :   }
     138             : 
     139             :   /**
     140             :    * Creates an instance of {@code GroupNameNotes} for use when creating a new group.
     141             :    *
     142             :    * <p><strong>Note: </strong>The returned instance of {@code GroupNameNotes} has to be committed
     143             :    * via {@link #commit(com.google.gerrit.server.git.meta.MetaDataUpdate) commit(MetaDataUpdate)} in
     144             :    * order to claim the new name.
     145             :    *
     146             :    * @param projectName the name of the project which holds the commits of the notes
     147             :    * @param repository the repository which holds the commits of the notes
     148             :    * @param groupUuid the UUID of the new group
     149             :    * @param groupName the name of the new group
     150             :    * @return an instance of {@code GroupNameNotes} configured for a specific group creation
     151             :    * @throws IOException if the repository can't be accessed for some reason
     152             :    * @throws ConfigInvalidException in no case so far
     153             :    * @throws DuplicateKeyException if a group with the new name already exists
     154             :    */
     155             :   public static GroupNameNotes forNewGroup(
     156             :       Project.NameKey projectName,
     157             :       Repository repository,
     158             :       AccountGroup.UUID groupUuid,
     159             :       AccountGroup.NameKey groupName)
     160             :       throws IOException, ConfigInvalidException, DuplicateKeyException {
     161         152 :     requireNonNull(groupName);
     162             : 
     163         152 :     GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, null, groupName);
     164         152 :     groupNameNotes.load(projectName, repository);
     165         152 :     groupNameNotes.ensureNewNameIsNotUsed();
     166         152 :     return groupNameNotes;
     167             :   }
     168             : 
     169             :   /**
     170             :    * Loads the {@code GroupReference} (name/UUID pair) for the group with the specified name.
     171             :    *
     172             :    * @param repository the repository which holds the commits of the notes
     173             :    * @param groupName the name of the group
     174             :    * @return the corresponding {@code GroupReference} if a group/note with the given name exists
     175             :    * @throws IOException if the repository can't be accessed for some reason
     176             :    * @throws ConfigInvalidException if the note for the specified group is in an invalid state
     177             :    */
     178             :   public static Optional<GroupReference> loadGroup(
     179             :       Repository repository, AccountGroup.NameKey groupName)
     180             :       throws IOException, ConfigInvalidException {
     181         152 :     Ref ref = repository.exactRef(RefNames.REFS_GROUPNAMES);
     182         152 :     if (ref == null) {
     183           2 :       return Optional.empty();
     184             :     }
     185             : 
     186         152 :     try (RevWalk revWalk = new RevWalk(repository);
     187         152 :         ObjectReader reader = revWalk.getObjectReader()) {
     188         152 :       RevCommit notesCommit = revWalk.parseCommit(ref.getObjectId());
     189         152 :       NoteMap noteMap = NoteMap.read(reader, notesCommit);
     190         152 :       ObjectId noteDataBlobId = noteMap.get(getNoteKey(groupName));
     191         152 :       if (noteDataBlobId == null) {
     192           1 :         return Optional.empty();
     193             :       }
     194         152 :       return Optional.of(getGroupReference(reader, noteDataBlobId));
     195           1 :     }
     196             :   }
     197             : 
     198             :   /**
     199             :    * Loads the {@code GroupReference}s (name/UUID pairs) for all groups.
     200             :    *
     201             :    * <p>Even though group UUIDs should be unique, this class doesn't enforce it. For this reason,
     202             :    * it's technically possible that two of the {@code GroupReference}s have a duplicate UUID but a
     203             :    * different name. In practice, this shouldn't occur unless we introduce a bug in the future.
     204             :    *
     205             :    * @param repository the repository which holds the commits of the notes
     206             :    * @return the {@code GroupReference}s of all existing groups/notes
     207             :    * @throws IOException if the repository can't be accessed for some reason
     208             :    * @throws ConfigInvalidException if one of the notes is in an invalid state
     209             :    */
     210             :   public static ImmutableList<GroupReference> loadAllGroups(Repository repository)
     211             :       throws IOException, ConfigInvalidException {
     212         146 :     Ref ref = repository.exactRef(RefNames.REFS_GROUPNAMES);
     213         146 :     if (ref == null) {
     214           1 :       return ImmutableList.of();
     215             :     }
     216         146 :     try (TraceTimer ignored =
     217         146 :             TraceContext.newTimer(
     218             :                 "Loading all groups",
     219         146 :                 Metadata.builder().noteDbRefName(RefNames.REFS_GROUPNAMES).build());
     220         146 :         RevWalk revWalk = new RevWalk(repository);
     221         146 :         ObjectReader reader = revWalk.getObjectReader()) {
     222         146 :       RevCommit notesCommit = revWalk.parseCommit(ref.getObjectId());
     223         146 :       NoteMap noteMap = NoteMap.read(reader, notesCommit);
     224             : 
     225         146 :       Multiset<GroupReference> groupReferences = HashMultiset.create();
     226         146 :       for (Note note : noteMap) {
     227         146 :         GroupReference groupReference = getGroupReference(reader, note.getData());
     228         146 :         int numOfOccurrences = groupReferences.add(groupReference, 1);
     229         146 :         if (numOfOccurrences > 1) {
     230           0 :           GroupsNoteDbConsistencyChecker.logConsistencyProblemAsWarning(
     231             :               "The UUID of group %s (%s) is duplicate in group name notes",
     232           0 :               groupReference.getName(), groupReference.getUUID());
     233             :         }
     234         146 :       }
     235             : 
     236         146 :       return ImmutableList.copyOf(groupReferences);
     237             :     }
     238             :   }
     239             : 
     240             :   /**
     241             :    * Replaces the map of name/UUID pairs with a new version which matches exactly the passed {@code
     242             :    * GroupReference}s.
     243             :    *
     244             :    * <p>All old entries are discarded and replaced by the new ones.
     245             :    *
     246             :    * <p>This operation also works if the previous map has invalid entries or can't be read anymore.
     247             :    *
     248             :    * <p><strong>Note: </strong>This method doesn't flush the {@code ObjectInserter}. It doesn't
     249             :    * execute the {@code BatchRefUpdate} either.
     250             :    *
     251             :    * @param repository the repository which holds the commits of the notes
     252             :    * @param inserter an {@code ObjectInserter} for that repository
     253             :    * @param bru a {@code BatchRefUpdate} to which this method adds commands
     254             :    * @param groupReferences all {@code GroupReference}s (name/UUID pairs) which should be contained
     255             :    *     in the map of name/UUID pairs
     256             :    * @param ident the {@code PersonIdent} which is used as author and committer for commits
     257             :    * @throws IOException if the repository can't be accessed for some reason
     258             :    */
     259             :   public static void updateAllGroups(
     260             :       Repository repository,
     261             :       ObjectInserter inserter,
     262             :       BatchRefUpdate bru,
     263             :       Collection<GroupReference> groupReferences,
     264             :       PersonIdent ident)
     265             :       throws IOException {
     266             :     // Not strictly necessary for iteration; throws IAE if it encounters duplicates, which is nice.
     267           1 :     ImmutableBiMap<AccountGroup.UUID, String> biMap = toBiMap(groupReferences);
     268             : 
     269           1 :     try (ObjectReader reader = inserter.newReader();
     270           1 :         RevWalk rw = new RevWalk(reader)) {
     271             :       // Always start from an empty map, discarding old notes.
     272           1 :       NoteMap noteMap = NoteMap.newEmptyMap();
     273           1 :       Ref ref = repository.exactRef(RefNames.REFS_GROUPNAMES);
     274           1 :       RevCommit oldCommit = ref != null ? rw.parseCommit(ref.getObjectId()) : null;
     275             : 
     276           1 :       for (Map.Entry<AccountGroup.UUID, String> e : biMap.entrySet()) {
     277           1 :         AccountGroup.NameKey nameKey = AccountGroup.nameKey(e.getValue());
     278           1 :         ObjectId noteKey = getNoteKey(nameKey);
     279           1 :         noteMap.set(noteKey, getAsNoteData(e.getKey(), nameKey), inserter);
     280           1 :       }
     281             : 
     282           1 :       ObjectId newTreeId = noteMap.writeTree(inserter);
     283           1 :       if (oldCommit != null && newTreeId.equals(oldCommit.getTree())) {
     284           1 :         return;
     285             :       }
     286           1 :       CommitBuilder cb = new CommitBuilder();
     287           1 :       if (oldCommit != null) {
     288           1 :         cb.addParentId(oldCommit);
     289             :       }
     290           1 :       cb.setTreeId(newTreeId);
     291           1 :       cb.setAuthor(ident);
     292           1 :       cb.setCommitter(ident);
     293           1 :       int n = groupReferences.size();
     294           1 :       cb.setMessage("Store " + n + " group name" + (n != 1 ? "s" : ""));
     295           1 :       ObjectId newId = inserter.insert(cb).copy();
     296             : 
     297           1 :       ObjectId oldId = ObjectIds.copyOrZero(oldCommit);
     298           1 :       bru.addCommand(new ReceiveCommand(oldId, newId, RefNames.REFS_GROUPNAMES));
     299           1 :     }
     300           1 :   }
     301             : 
     302             :   // Returns UUID <=> Name bimap.
     303             :   private static ImmutableBiMap<AccountGroup.UUID, String> toBiMap(
     304             :       Collection<GroupReference> groupReferences) {
     305             :     try {
     306           1 :       return groupReferences.stream()
     307           1 :           .collect(toImmutableBiMap(GroupReference::getUUID, GroupReference::getName));
     308           1 :     } catch (IllegalArgumentException e) {
     309           1 :       throw new IllegalArgumentException(UNIQUE_REF_ERROR, e);
     310             :     }
     311             :   }
     312             : 
     313             :   private final AccountGroup.UUID groupUuid;
     314             :   private Optional<AccountGroup.NameKey> oldGroupName;
     315             :   private Optional<AccountGroup.NameKey> newGroupName;
     316             : 
     317             :   private boolean nameConflicting;
     318             : 
     319             :   private GroupNameNotes(
     320             :       AccountGroup.UUID groupUuid,
     321             :       @Nullable AccountGroup.NameKey oldGroupName,
     322         152 :       @Nullable AccountGroup.NameKey newGroupName) {
     323         152 :     this.groupUuid = requireNonNull(groupUuid);
     324             : 
     325         152 :     if (Objects.equals(oldGroupName, newGroupName)) {
     326           1 :       this.oldGroupName = Optional.empty();
     327           1 :       this.newGroupName = Optional.empty();
     328             :     } else {
     329         152 :       this.oldGroupName = Optional.ofNullable(oldGroupName);
     330         152 :       this.newGroupName = Optional.ofNullable(newGroupName);
     331             :     }
     332         152 :   }
     333             : 
     334             :   @Override
     335             :   protected String getRefName() {
     336         152 :     return RefNames.REFS_GROUPNAMES;
     337             :   }
     338             : 
     339             :   @Override
     340             :   protected void onLoad() throws IOException, ConfigInvalidException {
     341         152 :     nameConflicting = false;
     342             : 
     343         152 :     logger.atFine().log("Reading group notes");
     344             : 
     345         152 :     if (revision != null) {
     346         152 :       NoteMap noteMap = NoteMap.read(reader, revision);
     347         152 :       if (newGroupName.isPresent()) {
     348         152 :         ObjectId newNameId = getNoteKey(newGroupName.get());
     349         152 :         nameConflicting = noteMap.contains(newNameId);
     350             :       }
     351         152 :       ensureOldNameIsPresent(noteMap);
     352             :     }
     353         152 :   }
     354             : 
     355             :   private void ensureOldNameIsPresent(NoteMap noteMap) throws IOException, ConfigInvalidException {
     356         152 :     if (oldGroupName.isPresent()) {
     357           4 :       AccountGroup.NameKey oldName = oldGroupName.get();
     358           4 :       ObjectId noteKey = getNoteKey(oldName);
     359           4 :       ObjectId noteDataBlobId = noteMap.get(noteKey);
     360           4 :       if (noteDataBlobId == null) {
     361           1 :         throw new ConfigInvalidException(
     362           1 :             String.format("Group name '%s' doesn't exist in the list of all names", oldName));
     363             :       }
     364           4 :       GroupReference group = getGroupReference(reader, noteDataBlobId);
     365           4 :       AccountGroup.UUID foundUuid = group.getUUID();
     366           4 :       if (!Objects.equals(groupUuid, foundUuid)) {
     367           1 :         throw new ConfigInvalidException(
     368           1 :             String.format(
     369             :                 "Name '%s' points to UUID '%s' and not to '%s'", oldName, foundUuid, groupUuid));
     370             :       }
     371             :     }
     372         152 :   }
     373             : 
     374             :   private void ensureNewNameIsNotUsed() throws DuplicateKeyException {
     375         152 :     if (newGroupName.isPresent() && nameConflicting) {
     376           2 :       throw new DuplicateKeyException(
     377           2 :           String.format("Name '%s' is already used", newGroupName.get().get()));
     378             :     }
     379         152 :   }
     380             : 
     381             :   @Override
     382             :   protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
     383         152 :     if (!oldGroupName.isPresent() && !newGroupName.isPresent()) {
     384           1 :       return false;
     385             :     }
     386             : 
     387         152 :     logger.atFine().log("Updating group notes");
     388             : 
     389         152 :     NoteMap noteMap = revision == null ? NoteMap.newEmptyMap() : NoteMap.read(reader, revision);
     390         152 :     if (oldGroupName.isPresent()) {
     391           4 :       removeNote(noteMap, oldGroupName.get(), inserter);
     392             :     }
     393             : 
     394         152 :     if (newGroupName.isPresent()) {
     395         152 :       addNote(noteMap, newGroupName.get(), groupUuid, inserter);
     396             :     }
     397             : 
     398         152 :     commit.setTreeId(noteMap.writeTree(inserter));
     399         152 :     commit.setMessage(getCommitMessage());
     400             : 
     401         152 :     oldGroupName = Optional.empty();
     402         152 :     newGroupName = Optional.empty();
     403             : 
     404         152 :     return true;
     405             :   }
     406             : 
     407             :   private static void removeNote(
     408             :       NoteMap noteMap, AccountGroup.NameKey groupName, ObjectInserter inserter) throws IOException {
     409           4 :     ObjectId noteKey = getNoteKey(groupName);
     410           4 :     noteMap.set(noteKey, null, inserter);
     411           4 :   }
     412             : 
     413             :   private static void addNote(
     414             :       NoteMap noteMap,
     415             :       AccountGroup.NameKey groupName,
     416             :       AccountGroup.UUID groupUuid,
     417             :       ObjectInserter inserter)
     418             :       throws IOException {
     419         152 :     ObjectId noteKey = getNoteKey(groupName);
     420         152 :     noteMap.set(noteKey, getAsNoteData(groupUuid, groupName), inserter);
     421         152 :   }
     422             : 
     423             :   // Use the same approach as ExternalId.Key.sha1().
     424             :   @SuppressWarnings("deprecation")
     425             :   @VisibleForTesting
     426             :   public static ObjectId getNoteKey(AccountGroup.NameKey groupName) {
     427         152 :     return ObjectId.fromRaw(Hashing.sha1().hashString(groupName.get(), UTF_8).asBytes());
     428             :   }
     429             : 
     430             :   private static String getAsNoteData(AccountGroup.UUID uuid, AccountGroup.NameKey groupName) {
     431         152 :     Config config = new Config();
     432         152 :     config.setString(SECTION_NAME, null, UUID_PARAM, uuid.get());
     433         152 :     config.setString(SECTION_NAME, null, NAME_PARAM, groupName.get());
     434         152 :     return config.toText();
     435             :   }
     436             : 
     437             :   private static GroupReference getGroupReference(ObjectReader reader, ObjectId noteDataBlobId)
     438             :       throws IOException, ConfigInvalidException {
     439         152 :     byte[] noteData = reader.open(noteDataBlobId, OBJ_BLOB).getCachedBytes();
     440         152 :     return getFromNoteData(noteData);
     441             :   }
     442             : 
     443             :   static GroupReference getFromNoteData(byte[] noteData) throws ConfigInvalidException {
     444         152 :     Config config = new Config();
     445         152 :     config.fromText(new String(noteData, UTF_8));
     446             : 
     447         152 :     String uuid = config.getString(SECTION_NAME, null, UUID_PARAM);
     448         152 :     String name = Strings.nullToEmpty(config.getString(SECTION_NAME, null, NAME_PARAM));
     449         152 :     if (uuid == null) {
     450           0 :       throw new ConfigInvalidException(String.format("UUID for group '%s' must be defined", name));
     451             :     }
     452             : 
     453         152 :     return GroupReference.create(AccountGroup.uuid(uuid), name);
     454             :   }
     455             : 
     456             :   private String getCommitMessage() {
     457         152 :     if (oldGroupName.isPresent() && newGroupName.isPresent()) {
     458           4 :       return String.format(
     459           4 :           "Rename group from '%s' to '%s'", oldGroupName.get(), newGroupName.get());
     460             :     }
     461         152 :     if (newGroupName.isPresent()) {
     462         152 :       return String.format("Create group '%s'", newGroupName.get());
     463             :     }
     464           0 :     if (oldGroupName.isPresent()) {
     465           0 :       return String.format("Delete group '%s'", oldGroupName.get());
     466             :     }
     467           0 :     return "No-op";
     468             :   }
     469             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750