LCOV - code coverage report
Current view: top level - server/group/db - GroupConfig.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 140 142 98.6 %
Date: 2022-11-19 15:00:39 Functions: 39 39 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.base.Preconditions.checkState;
      18             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      19             : import static java.util.Objects.requireNonNull;
      20             : import static java.util.stream.Collectors.joining;
      21             : 
      22             : import com.google.common.annotations.VisibleForTesting;
      23             : import com.google.common.base.Splitter;
      24             : import com.google.common.base.Strings;
      25             : import com.google.common.collect.ImmutableSet;
      26             : import com.google.common.collect.Streams;
      27             : import com.google.gerrit.common.Nullable;
      28             : import com.google.gerrit.entities.Account;
      29             : import com.google.gerrit.entities.AccountGroup;
      30             : import com.google.gerrit.entities.InternalGroup;
      31             : import com.google.gerrit.entities.Project;
      32             : import com.google.gerrit.entities.RefNames;
      33             : import com.google.gerrit.exceptions.DuplicateKeyException;
      34             : import com.google.gerrit.server.git.meta.MetaDataUpdate;
      35             : import com.google.gerrit.server.git.meta.VersionedMetaData;
      36             : import com.google.gerrit.server.util.time.TimeUtil;
      37             : import java.io.IOException;
      38             : import java.time.Instant;
      39             : import java.util.Arrays;
      40             : import java.util.Optional;
      41             : import java.util.function.Function;
      42             : import java.util.regex.Pattern;
      43             : import org.eclipse.jgit.errors.ConfigInvalidException;
      44             : import org.eclipse.jgit.lib.CommitBuilder;
      45             : import org.eclipse.jgit.lib.Config;
      46             : import org.eclipse.jgit.lib.ObjectId;
      47             : import org.eclipse.jgit.lib.PersonIdent;
      48             : import org.eclipse.jgit.lib.Repository;
      49             : import org.eclipse.jgit.revwalk.RevCommit;
      50             : import org.eclipse.jgit.revwalk.RevSort;
      51             : 
      52             : /**
      53             :  * A representation of a group in NoteDb.
      54             :  *
      55             :  * <p>Groups in NoteDb can be created by following the descriptions of {@link
      56             :  * #createForNewGroup(Project.NameKey, Repository, InternalGroupCreation)}. For reading groups from
      57             :  * NoteDb or updating them, refer to {@link #loadForGroup(Project.NameKey, Repository,
      58             :  * AccountGroup.UUID)} or {@link #loadForGroupSnapshot(Project.NameKey, Repository,
      59             :  * AccountGroup.UUID, ObjectId)}.
      60             :  *
      61             :  * <p><strong>Note:</strong> Any modification (group creation or update) only becomes permanent (and
      62             :  * hence written to NoteDb) if {@link #commit(MetaDataUpdate)} is called.
      63             :  *
      64             :  * <p><strong>Warning:</strong> This class is a low-level API for groups in NoteDb. Most code which
      65             :  * deals with internal Gerrit groups should use {@link Groups} or {@link GroupsUpdate} instead.
      66             :  *
      67             :  * <h2>Internal details</h2>
      68             :  *
      69             :  * <p>Each group is represented by a commit on a branch as defined by {@link
      70             :  * RefNames#refsGroups(AccountGroup.UUID)}. Previous versions of the group exist as older commits on
      71             :  * the same branch and can be reached by following along the parent references. New commits for
      72             :  * updates are only created if a real modification occurs.
      73             :  *
      74             :  * <p>The commit messages of all commits on that branch form the audit log for the group. The
      75             :  * messages mention any important modifications which happened for the group to avoid costly
      76             :  * computations.
      77             :  *
      78             :  * <p>Within each commit, the properties of a group are spread across three files:
      79             :  *
      80             :  * <ul>
      81             :  *   <li><em>group.config</em>, which holds all basic properties of a group (further specified by
      82             :  *       {@link GroupConfigEntry}), formatted as a JGit {@link Config} file
      83             :  *   <li><em>members</em>, which lists all members (accounts) of a group, formatted as one numeric
      84             :  *       ID per line
      85             :  *   <li><em>subgroups</em>, which lists all subgroups of a group, formatted as one UUID per line
      86             :  * </ul>
      87             :  *
      88             :  * <p>The files <em>members</em> and <em>subgroups</em> need not exist, which means that the group
      89             :  * doesn't have any members or subgroups.
      90             :  */
      91             : public class GroupConfig extends VersionedMetaData {
      92             :   @VisibleForTesting public static final String GROUP_CONFIG_FILE = "group.config";
      93             :   @VisibleForTesting static final String MEMBERS_FILE = "members";
      94             :   @VisibleForTesting static final String SUBGROUPS_FILE = "subgroups";
      95         152 :   private static final Pattern LINE_SEPARATOR_PATTERN = Pattern.compile("\\R");
      96             : 
      97             :   /**
      98             :    * Creates a {@link GroupConfig} for a new group from the {@link InternalGroupCreation} blueprint.
      99             :    * Further, optional properties can be specified by setting a {@link GroupDelta} via {@link
     100             :    * #setGroupDelta(GroupDelta, AuditLogFormatter)} on the returned {@link GroupConfig}.
     101             :    *
     102             :    * <p><strong>Note:</strong> The returned {@link GroupConfig} has to be committed via {@link
     103             :    * #commit(MetaDataUpdate)} in order to create the group for real.
     104             :    *
     105             :    * @param projectName the name of the project which holds the NoteDb commits for groups
     106             :    * @param repository the repository which holds the NoteDb commits for groups
     107             :    * @param groupCreation an {@link InternalGroupCreation} specifying all properties which are
     108             :    *     required for a new group
     109             :    * @return a {@link GroupConfig} for a group creation
     110             :    * @throws IOException if the repository can't be accessed for some reason
     111             :    * @throws ConfigInvalidException if a group with the same UUID already exists but can't be read
     112             :    *     due to an invalid format
     113             :    * @throws DuplicateKeyException if a group with the same UUID already exists
     114             :    */
     115             :   public static GroupConfig createForNewGroup(
     116             :       Project.NameKey projectName, Repository repository, InternalGroupCreation groupCreation)
     117             :       throws IOException, ConfigInvalidException, DuplicateKeyException {
     118         152 :     GroupConfig groupConfig = new GroupConfig(groupCreation.getGroupUUID());
     119         152 :     groupConfig.load(projectName, repository);
     120         152 :     groupConfig.setGroupCreation(groupCreation);
     121         152 :     return groupConfig;
     122             :   }
     123             : 
     124             :   /**
     125             :    * Creates a {@link GroupConfig} for an existing group.
     126             :    *
     127             :    * <p>The group is automatically loaded within this method and can be accessed via {@link
     128             :    * #getLoadedGroup()}.
     129             :    *
     130             :    * <p>It's safe to call this method for non-existing groups. In that case, {@link
     131             :    * #getLoadedGroup()} won't return any group. Thus, the existence of a group can be easily tested.
     132             :    *
     133             :    * <p>The group represented by the returned {@link GroupConfig} can be updated by setting an
     134             :    * {@link GroupDelta} via {@link #setGroupDelta(GroupDelta, AuditLogFormatter)} and committing the
     135             :    * {@link GroupConfig} via {@link #commit(MetaDataUpdate)}.
     136             :    *
     137             :    * @param projectName the name of the project which holds the NoteDb commits for groups
     138             :    * @param repository the repository which holds the NoteDb commits for groups
     139             :    * @param groupUuid the UUID of the group
     140             :    * @return a {@link GroupConfig} for the group with the specified UUID
     141             :    * @throws IOException if the repository can't be accessed for some reason
     142             :    * @throws ConfigInvalidException if the group exists but can't be read due to an invalid format
     143             :    */
     144             :   public static GroupConfig loadForGroup(
     145             :       Project.NameKey projectName, Repository repository, AccountGroup.UUID groupUuid)
     146             :       throws IOException, ConfigInvalidException {
     147         152 :     return loadForGroup(projectName, repository, groupUuid, null);
     148             :   }
     149             : 
     150             :   /**
     151             :    * Load the group for a specific revision.
     152             :    *
     153             :    * @see GroupConfig#loadForGroup(Project.NameKey, Repository, AccountGroup.UUID)
     154             :    */
     155             :   public static GroupConfig loadForGroup(
     156             :       Project.NameKey projectName,
     157             :       Repository repository,
     158             :       AccountGroup.UUID groupUuid,
     159             :       @Nullable ObjectId groupRefObjectId)
     160             :       throws IOException, ConfigInvalidException {
     161         152 :     GroupConfig groupConfig = new GroupConfig(groupUuid);
     162         152 :     if (groupRefObjectId == null) {
     163         152 :       groupConfig.load(projectName, repository);
     164             :     } else {
     165         151 :       groupConfig.load(projectName, repository, groupRefObjectId);
     166             :     }
     167         152 :     return groupConfig;
     168             :   }
     169             : 
     170             :   /**
     171             :    * Creates a {@link GroupConfig} for an existing group at a specific revision of the repository.
     172             :    *
     173             :    * <p>This method behaves nearly the same as {@link #loadForGroup(Project.NameKey, Repository,
     174             :    * AccountGroup.UUID)}. The only difference is that {@link #loadForGroup(Project.NameKey,
     175             :    * Repository, AccountGroup.UUID)} loads the group from the current state of the repository
     176             :    * whereas this method loads the group at a specific (maybe past) revision.
     177             :    *
     178             :    * @param projectName the name of the project which holds the NoteDb commits for groups
     179             :    * @param repository the repository which holds the NoteDb commits for groups
     180             :    * @param groupUuid the UUID of the group
     181             :    * @param commitId the revision of the repository at which the group should be loaded
     182             :    * @return a {@link GroupConfig} for the group with the specified UUID
     183             :    * @throws IOException if the repository can't be accessed for some reason
     184             :    * @throws ConfigInvalidException if the group exists but can't be read due to an invalid format
     185             :    */
     186             :   public static GroupConfig loadForGroupSnapshot(
     187             :       Project.NameKey projectName,
     188             :       Repository repository,
     189             :       AccountGroup.UUID groupUuid,
     190             :       ObjectId commitId)
     191             :       throws IOException, ConfigInvalidException {
     192           2 :     GroupConfig groupConfig = new GroupConfig(groupUuid);
     193           2 :     groupConfig.load(projectName, repository, commitId);
     194           2 :     return groupConfig;
     195             :   }
     196             : 
     197             :   private final AccountGroup.UUID groupUuid;
     198             :   private final String ref;
     199             : 
     200         152 :   private Optional<InternalGroup> loadedGroup = Optional.empty();
     201         152 :   private Optional<InternalGroupCreation> groupCreation = Optional.empty();
     202         152 :   private Optional<GroupDelta> groupDelta = Optional.empty();
     203         152 :   private AuditLogFormatter auditLogFormatter = AuditLogFormatter.createPartiallyWorkingFallBack();
     204         152 :   private boolean isLoaded = false;
     205             :   private boolean allowSaveEmptyName;
     206             : 
     207         152 :   private GroupConfig(AccountGroup.UUID groupUuid) {
     208         152 :     this.groupUuid = requireNonNull(groupUuid);
     209         152 :     ref = RefNames.refsGroups(groupUuid);
     210         152 :   }
     211             : 
     212             :   /**
     213             :    * Returns the group loaded from NoteDb.
     214             :    *
     215             :    * <p>If not any NoteDb commits exist for the group represented by this {@link GroupConfig}, no
     216             :    * group is returned.
     217             :    *
     218             :    * <p>After {@link #commit(MetaDataUpdate)} was called on this {@link GroupConfig}, this method
     219             :    * returns a group which is in line with the latest NoteDb commit for this group. So, after
     220             :    * creating a {@link GroupConfig} for a new group and committing it, this method can be used to
     221             :    * retrieve a representation of the created group. The same holds for the representation of an
     222             :    * updated group.
     223             :    *
     224             :    * @return the loaded group, or an empty {@link Optional} if the group doesn't exist
     225             :    */
     226             :   public Optional<InternalGroup> getLoadedGroup() {
     227         152 :     checkLoaded();
     228         152 :     return loadedGroup;
     229             :   }
     230             : 
     231             :   /**
     232             :    * Specifies how the current group should be updated.
     233             :    *
     234             :    * <p>If the group is newly created, the {@link GroupDelta} can be used to specify optional
     235             :    * properties.
     236             :    *
     237             :    * <p><strong>Note:</strong> This method doesn't perform the update. It only contains the
     238             :    * instructions for the update. To apply the update for real and write the result back to NoteDb,
     239             :    * call {@link #commit(MetaDataUpdate)} on this {@link GroupConfig}.
     240             :    *
     241             :    * @param groupDelta a {@link GroupDelta} with the modifications to be applied
     242             :    * @param auditLogFormatter an {@link AuditLogFormatter} for formatting the commit message in a
     243             :    *     parsable way
     244             :    */
     245             :   public void setGroupDelta(GroupDelta groupDelta, AuditLogFormatter auditLogFormatter) {
     246         152 :     this.groupDelta = Optional.of(groupDelta);
     247         152 :     this.auditLogFormatter = auditLogFormatter;
     248         152 :   }
     249             : 
     250             :   /**
     251             :    * Allows the new name of a group to be empty during creation or update.
     252             :    *
     253             :    * <p><strong>Note:</strong> This method exists only to support the migration of legacy groups
     254             :    * which don't always necessarily have a name. Nowadays, we enforce that groups always have names.
     255             :    * When we remove the migration code, we can probably remove this method as well.
     256             :    */
     257             :   public void setAllowSaveEmptyName() {
     258           1 :     this.allowSaveEmptyName = true;
     259           1 :   }
     260             : 
     261             :   private void setGroupCreation(InternalGroupCreation groupCreation) throws DuplicateKeyException {
     262         152 :     checkLoaded();
     263         152 :     if (loadedGroup.isPresent()) {
     264           0 :       throw new DuplicateKeyException(String.format("Group %s already exists", groupUuid.get()));
     265             :     }
     266             : 
     267         152 :     this.groupCreation = Optional.of(groupCreation);
     268         152 :   }
     269             : 
     270             :   @Override
     271             :   public String getRefName() {
     272         152 :     return ref;
     273             :   }
     274             : 
     275             :   @Override
     276             :   protected void onLoad() throws IOException, ConfigInvalidException {
     277         152 :     if (revision != null) {
     278         152 :       rw.reset();
     279         152 :       rw.markStart(revision);
     280         152 :       rw.sort(RevSort.REVERSE);
     281         152 :       RevCommit earliestCommit = rw.next();
     282         152 :       Instant createdOn = Instant.ofEpochSecond(earliestCommit.getCommitTime());
     283             : 
     284         152 :       Config config = readConfig(GROUP_CONFIG_FILE);
     285         152 :       ImmutableSet<Account.Id> members = readMembers();
     286         152 :       ImmutableSet<AccountGroup.UUID> subgroups = readSubgroups();
     287         152 :       loadedGroup =
     288         152 :           Optional.of(
     289         152 :               createFrom(groupUuid, config, members, subgroups, createdOn, revision.toObjectId()));
     290             :     }
     291             : 
     292         152 :     isLoaded = true;
     293         152 :   }
     294             : 
     295             :   @Override
     296             :   public RevCommit commit(MetaDataUpdate update) throws IOException {
     297         152 :     RevCommit c = super.commit(update);
     298         152 :     loadedGroup = Optional.of(loadedGroup.get().toBuilder().setRefState(c.toObjectId()).build());
     299         152 :     return c;
     300             :   }
     301             : 
     302             :   @Override
     303             :   protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
     304         152 :     checkLoaded();
     305         152 :     if (!groupCreation.isPresent() && !groupDelta.isPresent()) {
     306             :       // Group was neither created nor changed. -> A new commit isn't necessary.
     307           1 :       return false;
     308             :     }
     309             : 
     310         152 :     if (!allowSaveEmptyName && getNewName().equals(Optional.of(""))) {
     311           1 :       throw new ConfigInvalidException(
     312           1 :           String.format("Name of the group %s must be defined", groupUuid.get()));
     313             :     }
     314             : 
     315             :     // Commit timestamps are internally truncated to seconds. To return the correct 'createdOn' time
     316             :     // for new groups, we explicitly need to truncate the timestamp here.
     317         152 :     Instant commitTimestamp =
     318         152 :         TimeUtil.truncateToSecond(
     319         152 :             groupDelta.flatMap(GroupDelta::getUpdatedOn).orElseGet(TimeUtil::now));
     320         152 :     commit.setAuthor(new PersonIdent(commit.getAuthor(), commitTimestamp));
     321         152 :     commit.setCommitter(new PersonIdent(commit.getCommitter(), commitTimestamp));
     322             : 
     323         152 :     InternalGroup updatedGroup = updateGroup(commitTimestamp);
     324             : 
     325         152 :     String commitMessage = createCommitMessage(loadedGroup, updatedGroup);
     326         152 :     commit.setMessage(commitMessage);
     327             : 
     328         152 :     loadedGroup = Optional.of(updatedGroup);
     329         152 :     groupCreation = Optional.empty();
     330         152 :     groupDelta = Optional.empty();
     331             : 
     332         152 :     return true;
     333             :   }
     334             : 
     335             :   private void checkLoaded() {
     336         152 :     checkState(isLoaded, "Group %s not loaded yet", groupUuid.get());
     337         152 :   }
     338             : 
     339             :   private Optional<String> getNewName() {
     340         152 :     if (groupDelta.isPresent()) {
     341         152 :       return groupDelta.get().getName().map(n -> Strings.nullToEmpty(n.get()));
     342             :     }
     343           1 :     if (groupCreation.isPresent()) {
     344           1 :       return Optional.of(Strings.nullToEmpty(groupCreation.get().getNameKey().get()));
     345             :     }
     346           0 :     return Optional.empty();
     347             :   }
     348             : 
     349             :   private InternalGroup updateGroup(Instant commitTimestamp)
     350             :       throws IOException, ConfigInvalidException {
     351         152 :     Config config = updateGroupProperties();
     352             : 
     353         152 :     ImmutableSet<Account.Id> originalMembers =
     354         152 :         loadedGroup.map(InternalGroup::getMembers).orElseGet(ImmutableSet::of);
     355         152 :     Optional<ImmutableSet<Account.Id>> updatedMembers = updateMembers(originalMembers);
     356             : 
     357         152 :     ImmutableSet<AccountGroup.UUID> originalSubgroups =
     358         152 :         loadedGroup.map(InternalGroup::getSubgroups).orElseGet(ImmutableSet::of);
     359         152 :     Optional<ImmutableSet<AccountGroup.UUID>> updatedSubgroups = updateSubgroups(originalSubgroups);
     360             : 
     361         152 :     Instant createdOn = loadedGroup.map(InternalGroup::getCreatedOn).orElse(commitTimestamp);
     362             : 
     363         152 :     return createFrom(
     364             :         groupUuid,
     365             :         config,
     366         152 :         updatedMembers.orElse(originalMembers),
     367         152 :         updatedSubgroups.orElse(originalSubgroups),
     368             :         createdOn,
     369             :         null);
     370             :   }
     371             : 
     372             :   private Config updateGroupProperties() throws IOException, ConfigInvalidException {
     373         152 :     Config config = readConfig(GROUP_CONFIG_FILE);
     374         152 :     groupCreation.ifPresent(
     375             :         internalGroupCreation ->
     376         152 :             Arrays.stream(GroupConfigEntry.values())
     377         152 :                 .forEach(configEntry -> configEntry.initNewConfig(config, internalGroupCreation)));
     378         152 :     groupDelta.ifPresent(
     379             :         delta ->
     380         152 :             Arrays.stream(GroupConfigEntry.values())
     381         152 :                 .forEach(configEntry -> configEntry.updateConfigValue(config, delta)));
     382         152 :     saveConfig(GROUP_CONFIG_FILE, config);
     383         152 :     return config;
     384             :   }
     385             : 
     386             :   private Optional<ImmutableSet<Account.Id>> updateMembers(ImmutableSet<Account.Id> originalMembers)
     387             :       throws IOException {
     388         152 :     Optional<ImmutableSet<Account.Id>> updatedMembers =
     389             :         groupDelta
     390         152 :             .map(GroupDelta::getMemberModification)
     391         152 :             .map(memberModification -> memberModification.apply(originalMembers))
     392         152 :             .map(ImmutableSet::copyOf)
     393         152 :             .filter(members -> !originalMembers.equals(members));
     394         152 :     if (updatedMembers.isPresent()) {
     395         152 :       saveMembers(updatedMembers.get());
     396             :     }
     397         152 :     return updatedMembers;
     398             :   }
     399             : 
     400             :   private Optional<ImmutableSet<AccountGroup.UUID>> updateSubgroups(
     401             :       ImmutableSet<AccountGroup.UUID> originalSubgroups) throws IOException {
     402         152 :     Optional<ImmutableSet<AccountGroup.UUID>> updatedSubgroups =
     403             :         groupDelta
     404         152 :             .map(GroupDelta::getSubgroupModification)
     405         152 :             .map(subgroupModification -> subgroupModification.apply(originalSubgroups))
     406         152 :             .map(ImmutableSet::copyOf)
     407         152 :             .filter(subgroups -> !originalSubgroups.equals(subgroups));
     408         152 :     if (updatedSubgroups.isPresent()) {
     409           8 :       saveSubgroups(updatedSubgroups.get());
     410             :     }
     411         152 :     return updatedSubgroups;
     412             :   }
     413             : 
     414             :   private void saveMembers(ImmutableSet<Account.Id> members) throws IOException {
     415         152 :     saveToFile(MEMBERS_FILE, members, member -> String.valueOf(member.get()));
     416         152 :   }
     417             : 
     418             :   private void saveSubgroups(ImmutableSet<AccountGroup.UUID> subgroups) throws IOException {
     419           8 :     saveToFile(SUBGROUPS_FILE, subgroups, AccountGroup.UUID::get);
     420           8 :   }
     421             : 
     422             :   private <E> void saveToFile(
     423             :       String filePath, ImmutableSet<E> elements, Function<E, String> toStringFunction)
     424             :       throws IOException {
     425         152 :     String fileContent = elements.stream().map(toStringFunction).collect(joining("\n"));
     426         152 :     saveUTF8(filePath, fileContent);
     427         152 :   }
     428             : 
     429             :   private ImmutableSet<Account.Id> readMembers() throws IOException, ConfigInvalidException {
     430         152 :     return readFromFile(MEMBERS_FILE, entry -> Account.id(Integer.parseInt(entry)));
     431             :   }
     432             : 
     433             :   private ImmutableSet<AccountGroup.UUID> readSubgroups()
     434             :       throws IOException, ConfigInvalidException {
     435         152 :     return readFromFile(SUBGROUPS_FILE, AccountGroup::uuid);
     436             :   }
     437             : 
     438             :   private <E> ImmutableSet<E> readFromFile(String filePath, Function<String, E> fromStringFunction)
     439             :       throws IOException, ConfigInvalidException {
     440         152 :     String fileContent = readUTF8(filePath);
     441             :     try {
     442         152 :       Iterable<String> lines =
     443         152 :           Splitter.on(LINE_SEPARATOR_PATTERN).trimResults().omitEmptyStrings().split(fileContent);
     444         152 :       return Streams.stream(lines).map(fromStringFunction).collect(toImmutableSet());
     445           1 :     } catch (NumberFormatException e) {
     446           1 :       throw new ConfigInvalidException(
     447           1 :           String.format("Invalid file %s for commit %s", filePath, revision.name()), e);
     448             :     }
     449             :   }
     450             : 
     451             :   private static InternalGroup createFrom(
     452             :       AccountGroup.UUID groupUuid,
     453             :       Config config,
     454             :       ImmutableSet<Account.Id> members,
     455             :       ImmutableSet<AccountGroup.UUID> subgroups,
     456             :       Instant createdOn,
     457             :       ObjectId refState)
     458             :       throws ConfigInvalidException {
     459         152 :     InternalGroup.Builder group = InternalGroup.builder();
     460         152 :     group.setGroupUUID(groupUuid);
     461         152 :     for (GroupConfigEntry configEntry : GroupConfigEntry.values()) {
     462         152 :       configEntry.readFromConfig(groupUuid, group, config);
     463             :     }
     464         152 :     group.setMembers(members);
     465         152 :     group.setSubgroups(subgroups);
     466         152 :     group.setCreatedOn(createdOn);
     467         152 :     group.setRefState(refState);
     468         152 :     return group.build();
     469             :   }
     470             : 
     471             :   private String createCommitMessage(
     472             :       Optional<InternalGroup> originalGroup, InternalGroup updatedGroup) {
     473         152 :     GroupConfigCommitMessage commitMessage =
     474             :         new GroupConfigCommitMessage(auditLogFormatter, updatedGroup);
     475         152 :     originalGroup.ifPresent(commitMessage::setOriginalGroup);
     476         152 :     return commitMessage.create();
     477             :   }
     478             : }

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