LCOV - code coverage report
Current view: top level - server/git/validators - CommitValidators.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 368 422 87.2 %
Date: 2022-11-19 15:00:39 Functions: 47 49 95.9 %

          Line data    Source code
       1             : // Copyright (C) 2012 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.git.validators;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
      19             : import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
      20             : import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
      21             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      22             : import static java.util.stream.Collectors.toList;
      23             : 
      24             : import com.google.common.annotations.VisibleForTesting;
      25             : import com.google.common.base.Strings;
      26             : import com.google.common.collect.ImmutableList;
      27             : import com.google.common.flogger.FluentLogger;
      28             : import com.google.gerrit.common.FooterConstants;
      29             : import com.google.gerrit.common.Nullable;
      30             : import com.google.gerrit.entities.Account;
      31             : import com.google.gerrit.entities.BooleanProjectConfig;
      32             : import com.google.gerrit.entities.BranchNameKey;
      33             : import com.google.gerrit.entities.Change;
      34             : import com.google.gerrit.entities.RefNames;
      35             : import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
      36             : import com.google.gerrit.extensions.registration.DynamicItem;
      37             : import com.google.gerrit.extensions.restapi.AuthException;
      38             : import com.google.gerrit.server.ChangeUtil;
      39             : import com.google.gerrit.server.GerritPersonIdent;
      40             : import com.google.gerrit.server.IdentifiedUser;
      41             : import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
      42             : import com.google.gerrit.server.config.AllProjectsName;
      43             : import com.google.gerrit.server.config.AllUsersName;
      44             : import com.google.gerrit.server.config.GerritServerConfig;
      45             : import com.google.gerrit.server.config.UrlFormatter;
      46             : import com.google.gerrit.server.events.CommitReceivedEvent;
      47             : import com.google.gerrit.server.git.GitRepositoryManager;
      48             : import com.google.gerrit.server.git.ValidationError;
      49             : import com.google.gerrit.server.logging.Metadata;
      50             : import com.google.gerrit.server.logging.TraceContext;
      51             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      52             : import com.google.gerrit.server.patch.DiffOperations;
      53             : import com.google.gerrit.server.permissions.PermissionBackend;
      54             : import com.google.gerrit.server.permissions.PermissionBackendException;
      55             : import com.google.gerrit.server.permissions.RefPermission;
      56             : import com.google.gerrit.server.plugincontext.PluginSetContext;
      57             : import com.google.gerrit.server.project.LabelConfigValidator;
      58             : import com.google.gerrit.server.project.ProjectCache;
      59             : import com.google.gerrit.server.project.ProjectConfig;
      60             : import com.google.gerrit.server.project.ProjectState;
      61             : import com.google.gerrit.server.ssh.HostKey;
      62             : import com.google.gerrit.server.ssh.SshInfo;
      63             : import com.google.gerrit.server.util.MagicBranch;
      64             : import com.google.inject.Inject;
      65             : import com.google.inject.Singleton;
      66             : import java.io.IOException;
      67             : import java.net.MalformedURLException;
      68             : import java.net.URL;
      69             : import java.util.ArrayList;
      70             : import java.util.Collections;
      71             : import java.util.List;
      72             : import java.util.Optional;
      73             : import java.util.regex.Pattern;
      74             : import org.eclipse.jgit.diff.DiffEntry;
      75             : import org.eclipse.jgit.diff.DiffFormatter;
      76             : import org.eclipse.jgit.errors.ConfigInvalidException;
      77             : import org.eclipse.jgit.lib.Config;
      78             : import org.eclipse.jgit.lib.PersonIdent;
      79             : import org.eclipse.jgit.lib.Repository;
      80             : import org.eclipse.jgit.notes.NoteMap;
      81             : import org.eclipse.jgit.revwalk.FooterKey;
      82             : import org.eclipse.jgit.revwalk.FooterLine;
      83             : import org.eclipse.jgit.revwalk.RevCommit;
      84             : import org.eclipse.jgit.revwalk.RevWalk;
      85             : import org.eclipse.jgit.util.SystemReader;
      86             : import org.eclipse.jgit.util.io.DisabledOutputStream;
      87             : 
      88             : /**
      89             :  * Represents a list of {@link CommitValidationListener}s to run for a push to one branch of one
      90             :  * project.
      91             :  */
      92             : public class CommitValidators {
      93         110 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      94             : 
      95         110 :   public static final Pattern NEW_PATCHSET_PATTERN =
      96         110 :       Pattern.compile("^" + REFS_CHANGES + "(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/[1-9][0-9]*)?$");
      97             : 
      98             :   @Singleton
      99             :   public static class Factory {
     100             :     private final PersonIdent gerritIdent;
     101             :     private final DynamicItem<UrlFormatter> urlFormatter;
     102             :     private final PluginSetContext<CommitValidationListener> pluginValidators;
     103             :     private final GitRepositoryManager repoManager;
     104             :     private final AllUsersName allUsers;
     105             :     private final AllProjectsName allProjects;
     106             :     private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
     107             :     private final AccountValidator accountValidator;
     108             :     private final ProjectCache projectCache;
     109             :     private final ProjectConfig.Factory projectConfigFactory;
     110             :     private final DiffOperations diffOperations;
     111             :     private final Config config;
     112             : 
     113             :     @Inject
     114             :     Factory(
     115             :         @GerritPersonIdent PersonIdent gerritIdent,
     116             :         DynamicItem<UrlFormatter> urlFormatter,
     117             :         @GerritServerConfig Config config,
     118             :         PluginSetContext<CommitValidationListener> pluginValidators,
     119             :         GitRepositoryManager repoManager,
     120             :         AllUsersName allUsers,
     121             :         AllProjectsName allProjects,
     122             :         ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
     123             :         AccountValidator accountValidator,
     124             :         ProjectCache projectCache,
     125             :         ProjectConfig.Factory projectConfigFactory,
     126         146 :         DiffOperations diffOperations) {
     127         146 :       this.gerritIdent = gerritIdent;
     128         146 :       this.urlFormatter = urlFormatter;
     129         146 :       this.config = config;
     130         146 :       this.pluginValidators = pluginValidators;
     131         146 :       this.repoManager = repoManager;
     132         146 :       this.allUsers = allUsers;
     133         146 :       this.allProjects = allProjects;
     134         146 :       this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
     135         146 :       this.accountValidator = accountValidator;
     136         146 :       this.projectCache = projectCache;
     137         146 :       this.projectConfigFactory = projectConfigFactory;
     138         146 :       this.diffOperations = diffOperations;
     139         146 :     }
     140             : 
     141             :     public CommitValidators forReceiveCommits(
     142             :         PermissionBackend.ForProject forProject,
     143             :         BranchNameKey branch,
     144             :         IdentifiedUser user,
     145             :         SshInfo sshInfo,
     146             :         NoteMap rejectCommits,
     147             :         RevWalk rw,
     148             :         @Nullable Change change,
     149             :         boolean skipValidation) {
     150          96 :       PermissionBackend.ForRef perm = forProject.ref(branch.branch());
     151          96 :       ProjectState projectState =
     152          96 :           projectCache.get(branch.project()).orElseThrow(illegalState(branch.project()));
     153          96 :       ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
     154          96 :       validators
     155          96 :           .add(new UploadMergesPermissionValidator(perm))
     156          96 :           .add(new ProjectStateValidationListener(projectState))
     157          96 :           .add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
     158          96 :           .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
     159          96 :           .add(new FileCountValidator(repoManager, config))
     160          96 :           .add(new CommitterUploaderValidator(user, perm, urlFormatter.get()))
     161          96 :           .add(new SignedOffByValidator(user, perm, projectState))
     162          96 :           .add(
     163             :               new ChangeIdValidator(
     164          96 :                   projectState, user, urlFormatter.get(), config, sshInfo, change))
     165          96 :           .add(new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects))
     166          96 :           .add(new BannedCommitsValidator(rejectCommits))
     167          96 :           .add(new PluginCommitValidationListener(pluginValidators, skipValidation))
     168          96 :           .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker))
     169          96 :           .add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
     170          96 :           .add(new GroupCommitValidator(allUsers))
     171          96 :           .add(new LabelConfigValidator(diffOperations));
     172          96 :       return new CommitValidators(validators.build());
     173             :     }
     174             : 
     175             :     public CommitValidators forGerritCommits(
     176             :         PermissionBackend.ForProject forProject,
     177             :         BranchNameKey branch,
     178             :         IdentifiedUser user,
     179             :         SshInfo sshInfo,
     180             :         RevWalk rw,
     181             :         @Nullable Change change) {
     182          55 :       PermissionBackend.ForRef perm = forProject.ref(branch.branch());
     183          55 :       ProjectState projectState =
     184          55 :           projectCache.get(branch.project()).orElseThrow(illegalState(branch.project()));
     185          55 :       ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
     186          55 :       validators
     187          55 :           .add(new UploadMergesPermissionValidator(perm))
     188          55 :           .add(new ProjectStateValidationListener(projectState))
     189          55 :           .add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
     190          55 :           .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
     191          55 :           .add(new FileCountValidator(repoManager, config))
     192          55 :           .add(new SignedOffByValidator(user, perm, projectState))
     193          55 :           .add(
     194             :               new ChangeIdValidator(
     195          55 :                   projectState, user, urlFormatter.get(), config, sshInfo, change))
     196          55 :           .add(new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects))
     197          55 :           .add(new PluginCommitValidationListener(pluginValidators))
     198          55 :           .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker))
     199          55 :           .add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
     200          55 :           .add(new GroupCommitValidator(allUsers))
     201          55 :           .add(new LabelConfigValidator(diffOperations));
     202          55 :       return new CommitValidators(validators.build());
     203             :     }
     204             : 
     205             :     public CommitValidators forMergedCommits(
     206             :         PermissionBackend.ForProject forProject, BranchNameKey branch, IdentifiedUser user) {
     207             :       // Generally only include validators that are based on permissions of the
     208             :       // user creating a change for a merged commit; generally exclude
     209             :       // validators that would require amending the change in order to correct.
     210             :       //
     211             :       // Examples:
     212             :       //  - Change-Id and Signed-off-by can't be added to an already-merged
     213             :       //    commit.
     214             :       //  - If the commit is banned, we can't ban it here. In fact, creating a
     215             :       //    review of a previously merged and recently-banned commit is a use
     216             :       //    case for post-commit code review: so reviewers have a place to
     217             :       //    discuss what to do about it.
     218             :       //  - Plugin validators may do things like require certain commit message
     219             :       //    formats, so we play it safe and exclude them.
     220           3 :       PermissionBackend.ForRef perm = forProject.ref(branch.branch());
     221           3 :       ProjectState projectState =
     222           3 :           projectCache.get(branch.project()).orElseThrow(illegalState(branch.project()));
     223           3 :       ImmutableList.Builder<CommitValidationListener> validators = ImmutableList.builder();
     224           3 :       validators
     225           3 :           .add(new UploadMergesPermissionValidator(perm))
     226           3 :           .add(new ProjectStateValidationListener(projectState))
     227           3 :           .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
     228           3 :           .add(new CommitterUploaderValidator(user, perm, urlFormatter.get()));
     229           3 :       return new CommitValidators(validators.build());
     230             :     }
     231             :   }
     232             : 
     233             :   private final List<CommitValidationListener> validators;
     234             : 
     235         110 :   CommitValidators(List<CommitValidationListener> validators) {
     236         110 :     this.validators = validators;
     237         110 :   }
     238             : 
     239             :   public List<CommitValidationMessage> validate(CommitReceivedEvent receiveEvent)
     240             :       throws CommitValidationException {
     241         110 :     List<CommitValidationMessage> messages = new ArrayList<>();
     242             :     try {
     243         110 :       for (CommitValidationListener commitValidator : validators) {
     244         110 :         try (TraceTimer ignored =
     245         110 :             TraceContext.newTimer(
     246             :                 "Running CommitValidationListener",
     247         110 :                 Metadata.builder()
     248         110 :                     .className(commitValidator.getClass().getSimpleName())
     249         110 :                     .projectName(receiveEvent.getProjectNameKey().get())
     250         110 :                     .branchName(receiveEvent.getBranchNameKey().branch())
     251         110 :                     .commit(receiveEvent.commit.name())
     252         110 :                     .build())) {
     253         110 :           messages.addAll(commitValidator.onCommitReceived(receiveEvent));
     254             :         }
     255         110 :       }
     256          15 :     } catch (CommitValidationException e) {
     257          15 :       logger.atFine().withCause(e).log(
     258          15 :           "CommitValidationException occurred: %s", e.getFullMessage());
     259             :       // Keep the old messages (and their order) in case of an exception
     260          15 :       messages.addAll(e.getMessages());
     261          15 :       throw new CommitValidationException(e.getMessage(), messages);
     262         109 :     }
     263         109 :     return messages;
     264             :   }
     265             : 
     266             :   public static class ChangeIdValidator implements CommitValidationListener {
     267         110 :     private static final String CHANGE_ID_PREFIX = FooterConstants.CHANGE_ID.getName() + ":";
     268             :     private static final String MISSING_CHANGE_ID_MSG = "missing Change-Id in message footer";
     269             :     private static final String MISSING_SUBJECT_MSG =
     270             :         "missing subject; Change-Id must be in message footer";
     271             :     private static final String CHANGE_ID_ABOVE_FOOTER_MSG = "Change-Id must be in message footer";
     272             :     private static final String MULTIPLE_CHANGE_ID_MSG =
     273             :         "multiple Change-Id lines in message footer";
     274             :     private static final String INVALID_CHANGE_ID_MSG =
     275             :         "invalid Change-Id line format in message footer";
     276             : 
     277             :     @VisibleForTesting
     278             :     public static final String CHANGE_ID_MISMATCH_MSG =
     279             :         "Change-Id in message footer does not match Change-Id of target change";
     280             : 
     281         110 :     private static final Pattern CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN);
     282             : 
     283             :     private final ProjectState projectState;
     284             :     private final UrlFormatter urlFormatter;
     285             :     private final String installCommitMsgHookCommand;
     286             :     private final SshInfo sshInfo;
     287             :     private final IdentifiedUser user;
     288             :     private final Change change;
     289             : 
     290             :     public ChangeIdValidator(
     291             :         ProjectState projectState,
     292             :         IdentifiedUser user,
     293             :         UrlFormatter urlFormatter,
     294             :         Config config,
     295             :         SshInfo sshInfo,
     296         110 :         Change change) {
     297         110 :       this.projectState = projectState;
     298         110 :       this.user = user;
     299         110 :       this.urlFormatter = urlFormatter;
     300         110 :       installCommitMsgHookCommand = config.getString("gerrit", null, "installCommitMsgHookCommand");
     301         110 :       this.sshInfo = sshInfo;
     302         110 :       this.change = change;
     303         110 :     }
     304             : 
     305             :     @Override
     306             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     307             :         throws CommitValidationException {
     308         110 :       if (!shouldValidateChangeId(receiveEvent)) {
     309          68 :         return Collections.emptyList();
     310             :       }
     311         103 :       RevCommit commit = receiveEvent.commit;
     312         103 :       List<CommitValidationMessage> messages = new ArrayList<>();
     313         103 :       List<String> idList = ChangeUtil.getChangeIdsFromFooter(commit, urlFormatter);
     314             : 
     315         103 :       if (idList.isEmpty()) {
     316           4 :         String shortMsg = commit.getShortMessage();
     317           4 :         if (shortMsg.startsWith(CHANGE_ID_PREFIX)
     318           4 :             && CHANGE_ID.matcher(shortMsg.substring(CHANGE_ID_PREFIX.length()).trim()).matches()) {
     319           4 :           throw new CommitValidationException(MISSING_SUBJECT_MSG);
     320             :         }
     321           3 :         if (commit.getFullMessage().contains("\n" + CHANGE_ID_PREFIX)) {
     322           3 :           messages.add(
     323             :               new CommitValidationMessage(
     324             :                   CHANGE_ID_ABOVE_FOOTER_MSG
     325             :                       + "\n"
     326             :                       + "\n"
     327             :                       + "Hint: run\n"
     328             :                       + "  git commit --amend\n"
     329             :                       + "and move 'Change-Id: Ixxx..' to the bottom on a separate line\n",
     330             :                   ValidationMessage.Type.ERROR));
     331           3 :           throw new CommitValidationException(CHANGE_ID_ABOVE_FOOTER_MSG, messages);
     332             :         }
     333           3 :         if (projectState.is(BooleanProjectConfig.REQUIRE_CHANGE_ID)) {
     334           3 :           messages.add(getMissingChangeIdErrorMsg(MISSING_CHANGE_ID_MSG));
     335           3 :           throw new CommitValidationException(MISSING_CHANGE_ID_MSG, messages);
     336             :         }
     337         103 :       } else if (idList.size() > 1) {
     338           3 :         throw new CommitValidationException(MULTIPLE_CHANGE_ID_MSG, messages);
     339             :       } else {
     340         103 :         String v = idList.get(0).trim();
     341             :         // Reject Change-Ids with wrong format and invalid placeholder ID from
     342             :         // Egit (I0000000000000000000000000000000000000000).
     343         103 :         if (!CHANGE_ID.matcher(v).matches() || v.matches("^I00*$")) {
     344           4 :           messages.add(getMissingChangeIdErrorMsg(INVALID_CHANGE_ID_MSG));
     345           4 :           throw new CommitValidationException(INVALID_CHANGE_ID_MSG, messages);
     346             :         }
     347         103 :         if (change != null && !v.equals(change.getKey().get())) {
     348           0 :           throw new CommitValidationException(CHANGE_ID_MISMATCH_MSG);
     349             :         }
     350             :       }
     351             : 
     352         103 :       return Collections.emptyList();
     353             :     }
     354             : 
     355             :     private static boolean shouldValidateChangeId(CommitReceivedEvent event) {
     356         110 :       return MagicBranch.isMagicBranch(event.command.getRefName())
     357         110 :           || NEW_PATCHSET_PATTERN.matcher(event.command.getRefName()).matches();
     358             :     }
     359             : 
     360             :     private CommitValidationMessage getMissingChangeIdErrorMsg(String errMsg) {
     361           4 :       return new CommitValidationMessage(
     362             :           errMsg
     363             :               + "\n"
     364             :               + "\nHint: to automatically insert a Change-Id, install the hook:\n"
     365           4 :               + getCommitMessageHookInstallationHint()
     366             :               + "\n"
     367             :               + "and then amend the commit:\n"
     368             :               + "  git commit --amend --no-edit\n"
     369             :               + "Finally, push your changes again\n",
     370             :           ValidationMessage.Type.ERROR);
     371             :     }
     372             : 
     373             :     private String getCommitMessageHookInstallationHint() {
     374           4 :       if (installCommitMsgHookCommand != null) {
     375           0 :         return installCommitMsgHookCommand;
     376             :       }
     377           4 :       final List<HostKey> hostKeys = sshInfo.getHostKeys();
     378             : 
     379             :       // If there are no SSH keys, the commit-msg hook must be installed via
     380             :       // HTTP(S)
     381           4 :       Optional<String> webUrl = urlFormatter.getWebUrl();
     382             : 
     383           4 :       String httpHook =
     384           4 :           String.format(
     385             :               "f=\"$(git rev-parse --git-dir)/hooks/commit-msg\"; curl -o \"$f\" %stools/hooks/commit-msg ; chmod +x \"$f\"",
     386           4 :               webUrl.get());
     387             : 
     388           4 :       if (hostKeys.isEmpty()) {
     389           2 :         checkState(webUrl.isPresent());
     390           2 :         return httpHook;
     391             :       }
     392             : 
     393             :       // SSH keys exist, so the hook might be able to be installed with scp.
     394             :       String sshHost;
     395             :       int sshPort;
     396           2 :       String host = hostKeys.get(0).getHost();
     397           2 :       int c = host.lastIndexOf(':');
     398           2 :       if (0 <= c) {
     399           2 :         if (host.startsWith("*:")) {
     400           0 :           checkState(webUrl.isPresent());
     401           0 :           sshHost = getGerritHost(webUrl.get());
     402             :         } else {
     403           2 :           sshHost = host.substring(0, c);
     404             :         }
     405           2 :         sshPort = Integer.parseInt(host.substring(c + 1));
     406             :       } else {
     407           0 :         sshHost = host;
     408           0 :         sshPort = 22;
     409             :       }
     410             : 
     411             :       // TODO(15944): Remove once both SFTP/SCP protocol are supported.
     412             :       //
     413             :       // In newer versions of OpenSSH, the default hook installation command will fail with a
     414             :       // cryptic error because the scp binary defaults to a different protocol.
     415           2 :       String scpFlagHint = "(for OpenSSH >= 9.0 you need to add the flag '-O' to the scp command)";
     416             : 
     417           2 :       String sshHook =
     418           2 :           String.format(
     419             :               "gitdir=$(git rev-parse --git-dir); scp -p -P %d %s@%s:hooks/commit-msg ${gitdir}/hooks/",
     420           2 :               sshPort, user.getUserName().orElse("<USERNAME>"), sshHost);
     421           2 :       return String.format("  %s\n%s\nor, for http(s):\n  %s", sshHook, scpFlagHint, httpHook);
     422             :     }
     423             :   }
     424             : 
     425             :   /** Limits the number of files per change. */
     426             :   private static class FileCountValidator implements CommitValidationListener {
     427             : 
     428             :     private final GitRepositoryManager repoManager;
     429             :     private final int maxFileCount;
     430             : 
     431         110 :     FileCountValidator(GitRepositoryManager repoManager, Config config) {
     432         110 :       this.repoManager = repoManager;
     433         110 :       maxFileCount = config.getInt("change", null, "maxFiles", 100_000);
     434         110 :     }
     435             : 
     436             :     @Override
     437             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     438             :         throws CommitValidationException {
     439             :       // TODO(zieren): Refactor interface to signal the intent of the event instead of hard-coding
     440             :       // it here. Due to interface limitations, this method is called from both receive commits
     441             :       // and from main Gerrit (e.g. when publishing a change edit). This is why we need to gate the
     442             :       // early return on REFS_CHANGES (though pushes to refs/changes are not possible).
     443         110 :       String refName = receiveEvent.command.getRefName();
     444         110 :       if (!refName.startsWith("refs/for/") && !refName.startsWith(RefNames.REFS_CHANGES)) {
     445             :         // This is a direct push bypassing review. We don't need to enforce any file-count limits
     446             :         // here.
     447          47 :         return Collections.emptyList();
     448             :       }
     449             : 
     450             :       // Use DiffFormatter to compute the number of files in the change. This should be faster than
     451             :       // the previous approach of using the PatchListCache.
     452             :       try {
     453         104 :         long changedFiles = countChangedFiles(receiveEvent);
     454         104 :         if (changedFiles > maxFileCount) {
     455           3 :           throw new CommitValidationException(
     456           3 :               String.format(
     457             :                   "Exceeding maximum number of files per change (%d > %d)",
     458           3 :                   changedFiles, maxFileCount));
     459             :         }
     460           0 :       } catch (IOException e) {
     461             :         // This happens e.g. for cherrypicks.
     462           0 :         if (!receiveEvent.command.getRefName().startsWith(REFS_CHANGES)) {
     463           0 :           logger.atWarning().withCause(e).log(
     464             :               "Failed to validate file count for commit: %s", receiveEvent.commit);
     465             :         }
     466         103 :       }
     467         103 :       return Collections.emptyList();
     468             :     }
     469             : 
     470             :     private long countChangedFiles(CommitReceivedEvent receiveEvent) throws IOException {
     471         104 :       try (Repository repository = repoManager.openRepository(receiveEvent.project.getNameKey());
     472         104 :           DiffFormatter diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
     473         104 :         diffFormatter.setRepository(repository);
     474             :         // Do not detect renames; that would require reading file contents, which is slow for large
     475             :         // files.
     476         104 :         diffFormatter.setDetectRenames(false);
     477             :         // For merge commits, i.e. >1 parents, we use parent #0 by convention.
     478         104 :         List<DiffEntry> diffEntries =
     479         104 :             diffFormatter.scan(
     480         104 :                 receiveEvent.commit.getParentCount() > 0 ? receiveEvent.commit.getParent(0) : null,
     481             :                 receiveEvent.commit);
     482         104 :         return diffEntries.stream().map(DiffEntry::getNewPath).distinct().count();
     483             :       }
     484             :     }
     485             :   }
     486             : 
     487             :   /** If this is the special project configuration branch, validate the config. */
     488             :   public static class ConfigValidator implements CommitValidationListener {
     489             :     private final ProjectConfig.Factory projectConfigFactory;
     490             :     private final BranchNameKey branch;
     491             :     private final IdentifiedUser user;
     492             :     private final RevWalk rw;
     493             :     private final AllUsersName allUsers;
     494             :     private final AllProjectsName allProjects;
     495             : 
     496             :     public ConfigValidator(
     497             :         ProjectConfig.Factory projectConfigFactory,
     498             :         BranchNameKey branch,
     499             :         IdentifiedUser user,
     500             :         RevWalk rw,
     501             :         AllUsersName allUsers,
     502         110 :         AllProjectsName allProjects) {
     503         110 :       this.projectConfigFactory = projectConfigFactory;
     504         110 :       this.branch = branch;
     505         110 :       this.user = user;
     506         110 :       this.rw = rw;
     507         110 :       this.allProjects = allProjects;
     508         110 :       this.allUsers = allUsers;
     509         110 :     }
     510             : 
     511             :     @Override
     512             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     513             :         throws CommitValidationException {
     514         110 :       if (REFS_CONFIG.equals(branch.branch())) {
     515          14 :         List<CommitValidationMessage> messages = new ArrayList<>();
     516             : 
     517             :         try {
     518          14 :           ProjectConfig cfg = projectConfigFactory.create(receiveEvent.project.getNameKey());
     519          14 :           cfg.load(rw, receiveEvent.command.getNewId());
     520          14 :           if (!cfg.getValidationErrors().isEmpty()) {
     521           3 :             addError("Invalid project configuration:", messages);
     522           3 :             for (ValidationError err : cfg.getValidationErrors()) {
     523           3 :               addError("  " + err.getMessage(), messages);
     524           3 :             }
     525           3 :             throw new CommitValidationException("invalid project configuration", messages);
     526             :           }
     527          14 :           if (allUsers.equals(receiveEvent.project.getNameKey())
     528           1 :               && !allProjects.equals(cfg.getProject().getParent(allProjects))) {
     529           1 :             addError("Invalid project configuration:", messages);
     530           1 :             addError(
     531           1 :                 String.format("  %s must inherit from %s", allUsers.get(), allProjects.get()),
     532             :                 messages);
     533           1 :             throw new CommitValidationException("invalid project configuration", messages);
     534             :           }
     535           1 :         } catch (ConfigInvalidException | IOException e) {
     536           1 :           if (e instanceof ConfigInvalidException && !Strings.isNullOrEmpty(e.getMessage())) {
     537           1 :             addError(e.getMessage(), messages);
     538             :           }
     539           1 :           logger.atSevere().withCause(e).log(
     540             :               "User %s tried to push an invalid project configuration %s for project %s",
     541           1 :               user.getLoggableName(),
     542           1 :               receiveEvent.command.getNewId().name(),
     543           1 :               receiveEvent.project.getName());
     544           1 :           throw new CommitValidationException("invalid project configuration", messages);
     545          13 :         }
     546             :       }
     547             : 
     548         110 :       return Collections.emptyList();
     549             :     }
     550             :   }
     551             : 
     552             :   /** Require permission to upload merge commits. */
     553             :   public static class UploadMergesPermissionValidator implements CommitValidationListener {
     554             :     private final PermissionBackend.ForRef perm;
     555             : 
     556         110 :     public UploadMergesPermissionValidator(PermissionBackend.ForRef perm) {
     557         110 :       this.perm = perm;
     558         110 :     }
     559             : 
     560             :     @Override
     561             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     562             :         throws CommitValidationException {
     563         110 :       if (receiveEvent.commit.getParentCount() <= 1) {
     564         110 :         return Collections.emptyList();
     565             :       }
     566             :       try {
     567          26 :         if (perm.test(RefPermission.MERGE)) {
     568          26 :           return Collections.emptyList();
     569             :         }
     570           0 :         throw new CommitValidationException("you are not allowed to upload merges");
     571           0 :       } catch (PermissionBackendException e) {
     572           0 :         logger.atSevere().withCause(e).log("cannot check MERGE");
     573           0 :         throw new CommitValidationException("internal auth error");
     574             :       }
     575             :     }
     576             :   }
     577             : 
     578             :   /** Execute commit validation plug-ins */
     579             :   public static class PluginCommitValidationListener implements CommitValidationListener {
     580             :     private final boolean skipValidation;
     581             :     private final PluginSetContext<CommitValidationListener> commitValidationListeners;
     582             : 
     583             :     public PluginCommitValidationListener(
     584             :         final PluginSetContext<CommitValidationListener> commitValidationListeners) {
     585          55 :       this(commitValidationListeners, false);
     586          55 :     }
     587             : 
     588             :     public PluginCommitValidationListener(
     589             :         final PluginSetContext<CommitValidationListener> commitValidationListeners,
     590         110 :         boolean skipValidation) {
     591         110 :       this.skipValidation = skipValidation;
     592         110 :       this.commitValidationListeners = commitValidationListeners;
     593         110 :     }
     594             : 
     595             :     private void runValidator(
     596             :         CommitValidationListener validator,
     597             :         List<CommitValidationMessage> messages,
     598             :         CommitReceivedEvent receiveEvent)
     599             :         throws CommitValidationException {
     600         109 :       if (skipValidation && !validator.shouldValidateAllCommits()) {
     601           3 :         return;
     602             :       }
     603         109 :       messages.addAll(validator.onCommitReceived(receiveEvent));
     604         109 :     }
     605             : 
     606             :     @Override
     607             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     608             :         throws CommitValidationException {
     609         109 :       List<CommitValidationMessage> messages = new ArrayList<>();
     610             :       try {
     611         109 :         commitValidationListeners.runEach(
     612         109 :             l -> runValidator(l, messages, receiveEvent), CommitValidationException.class);
     613           1 :       } catch (CommitValidationException e) {
     614           1 :         messages.addAll(e.getMessages());
     615           1 :         throw new CommitValidationException(e.getMessage(), messages);
     616         109 :       }
     617         109 :       return messages;
     618             :     }
     619             : 
     620             :     @Override
     621             :     public boolean shouldValidateAllCommits() {
     622           0 :       return commitValidationListeners.stream()
     623           0 :           .anyMatch(CommitValidationListener::shouldValidateAllCommits);
     624             :     }
     625             :   }
     626             : 
     627             :   public static class SignedOffByValidator implements CommitValidationListener {
     628             :     private final IdentifiedUser user;
     629             :     private final PermissionBackend.ForRef perm;
     630             :     private final ProjectState state;
     631             : 
     632             :     public SignedOffByValidator(
     633         110 :         IdentifiedUser user, PermissionBackend.ForRef perm, ProjectState state) {
     634         110 :       this.user = user;
     635         110 :       this.perm = perm;
     636         110 :       this.state = state;
     637         110 :     }
     638             : 
     639             :     @Override
     640             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     641             :         throws CommitValidationException {
     642         110 :       if (!state.is(BooleanProjectConfig.USE_SIGNED_OFF_BY)) {
     643         110 :         return Collections.emptyList();
     644             :       }
     645             : 
     646           3 :       RevCommit commit = receiveEvent.commit;
     647           3 :       PersonIdent committer = commit.getCommitterIdent();
     648           3 :       PersonIdent author = commit.getAuthorIdent();
     649             : 
     650           3 :       boolean sboAuthor = false;
     651           3 :       boolean sboCommitter = false;
     652           3 :       boolean sboMe = false;
     653           3 :       for (FooterLine footer : commit.getFooterLines()) {
     654           3 :         if (footer.matches(FooterKey.SIGNED_OFF_BY)) {
     655           3 :           String e = footer.getEmailAddress();
     656           3 :           if (e != null) {
     657           3 :             sboAuthor |= author.getEmailAddress().equals(e);
     658           3 :             sboCommitter |= committer.getEmailAddress().equals(e);
     659           3 :             sboMe |= user.hasEmailAddress(e);
     660             :           }
     661             :         }
     662           3 :       }
     663           3 :       if (!sboAuthor && !sboCommitter && !sboMe) {
     664             :         try {
     665           3 :           if (!perm.test(RefPermission.FORGE_COMMITTER)) {
     666           3 :             throw new CommitValidationException(
     667             :                 "not Signed-off-by author/committer/uploader in message footer");
     668             :           }
     669           0 :         } catch (PermissionBackendException e) {
     670           0 :           logger.atSevere().withCause(e).log("cannot check FORGE_COMMITTER");
     671           0 :           throw new CommitValidationException("internal auth error");
     672           0 :         }
     673             :       }
     674           3 :       return Collections.emptyList();
     675             :     }
     676             :   }
     677             : 
     678             :   /** Require that author matches the uploader. */
     679             :   public static class AuthorUploaderValidator implements CommitValidationListener {
     680             :     private final IdentifiedUser user;
     681             :     private final PermissionBackend.ForRef perm;
     682             :     private final UrlFormatter urlFormatter;
     683             : 
     684             :     public AuthorUploaderValidator(
     685         110 :         IdentifiedUser user, PermissionBackend.ForRef perm, UrlFormatter urlFormatter) {
     686         110 :       this.user = user;
     687         110 :       this.perm = perm;
     688         110 :       this.urlFormatter = urlFormatter;
     689         110 :     }
     690             : 
     691             :     @Override
     692             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     693             :         throws CommitValidationException {
     694         110 :       PersonIdent author = receiveEvent.commit.getAuthorIdent();
     695         110 :       if (user.hasEmailAddress(author.getEmailAddress())) {
     696         106 :         return Collections.emptyList();
     697             :       }
     698             :       try {
     699          36 :         if (!perm.test(RefPermission.FORGE_AUTHOR)) {
     700           1 :           throw new CommitValidationException(
     701           1 :               "invalid author", invalidEmail("author", author, user, urlFormatter));
     702             :         }
     703          36 :         return Collections.emptyList();
     704           0 :       } catch (PermissionBackendException e) {
     705           0 :         logger.atSevere().withCause(e).log("cannot check FORGE_AUTHOR");
     706           0 :         throw new CommitValidationException("internal auth error");
     707             :       }
     708             :     }
     709             :   }
     710             : 
     711             :   /** Require that committer matches the uploader. */
     712             :   public static class CommitterUploaderValidator implements CommitValidationListener {
     713             :     private final IdentifiedUser user;
     714             :     private final PermissionBackend.ForRef perm;
     715             :     private final UrlFormatter urlFormatter;
     716             : 
     717             :     public CommitterUploaderValidator(
     718          96 :         IdentifiedUser user, PermissionBackend.ForRef perm, UrlFormatter urlFormatter) {
     719          96 :       this.user = user;
     720          96 :       this.perm = perm;
     721          96 :       this.urlFormatter = urlFormatter;
     722          96 :     }
     723             : 
     724             :     @Override
     725             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     726             :         throws CommitValidationException {
     727          96 :       PersonIdent committer = receiveEvent.commit.getCommitterIdent();
     728          96 :       if (user.hasEmailAddress(committer.getEmailAddress())) {
     729          93 :         return Collections.emptyList();
     730             :       }
     731             :       try {
     732          30 :         if (!perm.test(RefPermission.FORGE_COMMITTER)) {
     733           0 :           throw new CommitValidationException(
     734           0 :               "invalid committer", invalidEmail("committer", committer, user, urlFormatter));
     735             :         }
     736          30 :         return Collections.emptyList();
     737           0 :       } catch (PermissionBackendException e) {
     738           0 :         logger.atSevere().withCause(e).log("cannot check FORGE_COMMITTER");
     739           0 :         throw new CommitValidationException("internal auth error");
     740             :       }
     741             :     }
     742             :   }
     743             : 
     744             :   /**
     745             :    * Don't allow the user to amend a merge created by Gerrit Code Review. This seems to happen all
     746             :    * too often, due to users not paying any attention to what they are doing.
     747             :    */
     748             :   public static class AmendedGerritMergeCommitValidationListener
     749             :       implements CommitValidationListener {
     750             :     private final PermissionBackend.ForRef perm;
     751             :     private final PersonIdent gerritIdent;
     752             : 
     753             :     public AmendedGerritMergeCommitValidationListener(
     754         110 :         PermissionBackend.ForRef perm, PersonIdent gerritIdent) {
     755         110 :       this.perm = perm;
     756         110 :       this.gerritIdent = gerritIdent;
     757         110 :     }
     758             : 
     759             :     @Override
     760             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     761             :         throws CommitValidationException {
     762         110 :       PersonIdent author = receiveEvent.commit.getAuthorIdent();
     763         110 :       if (receiveEvent.commit.getParentCount() > 1
     764          26 :           && author.getName().equals(gerritIdent.getName())
     765           0 :           && author.getEmailAddress().equals(gerritIdent.getEmailAddress())) {
     766             :         try {
     767             :           // Stop authors from amending the merge commits that Gerrit itself creates.
     768           0 :           perm.check(RefPermission.FORGE_SERVER);
     769           0 :         } catch (AuthException denied) {
     770           0 :           throw new CommitValidationException(
     771           0 :               String.format(
     772             :                   "pushing merge commit %s by %s requires '%s' permission",
     773           0 :                   receiveEvent.commit.getId(),
     774           0 :                   gerritIdent.getEmailAddress(),
     775           0 :                   RefPermission.FORGE_SERVER.name()),
     776             :               denied);
     777           0 :         } catch (PermissionBackendException e) {
     778           0 :           logger.atSevere().withCause(e).log("cannot check FORGE_SERVER");
     779           0 :           throw new CommitValidationException("internal auth error");
     780           0 :         }
     781             :       }
     782         110 :       return Collections.emptyList();
     783             :     }
     784             :   }
     785             : 
     786             :   /** Reject banned commits. */
     787             :   public static class BannedCommitsValidator implements CommitValidationListener {
     788             :     private final NoteMap rejectCommits;
     789             : 
     790          96 :     public BannedCommitsValidator(NoteMap rejectCommits) {
     791          96 :       this.rejectCommits = rejectCommits;
     792          96 :     }
     793             : 
     794             :     @Override
     795             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     796             :         throws CommitValidationException {
     797             :       try {
     798          96 :         if (rejectCommits.contains(receiveEvent.commit)) {
     799           2 :           throw new CommitValidationException(
     800           2 :               "contains banned commit " + receiveEvent.commit.getName());
     801             :         }
     802          95 :         return Collections.emptyList();
     803           0 :       } catch (IOException e) {
     804           0 :         throw new CommitValidationException("error checking banned commits", e);
     805             :       }
     806             :     }
     807             :   }
     808             : 
     809             :   /** Validates updates to refs/meta/external-ids. */
     810             :   public static class ExternalIdUpdateListener implements CommitValidationListener {
     811             :     private final AllUsersName allUsers;
     812             :     private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
     813             : 
     814             :     public ExternalIdUpdateListener(
     815         110 :         AllUsersName allUsers, ExternalIdsConsistencyChecker externalIdsConsistencyChecker) {
     816         110 :       this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
     817         110 :       this.allUsers = allUsers;
     818         110 :     }
     819             : 
     820             :     @Override
     821             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     822             :         throws CommitValidationException {
     823         109 :       if (allUsers.equals(receiveEvent.project.getNameKey())
     824           6 :           && RefNames.REFS_EXTERNAL_IDS.equals(receiveEvent.refName)) {
     825             :         try {
     826           1 :           List<ConsistencyProblemInfo> problems =
     827           1 :               externalIdsConsistencyChecker.check(receiveEvent.commit);
     828           1 :           List<CommitValidationMessage> msgs =
     829           1 :               problems.stream()
     830           1 :                   .map(
     831             :                       p ->
     832           1 :                           new CommitValidationMessage(
     833             :                               p.message,
     834           1 :                               p.status == ConsistencyProblemInfo.Status.ERROR
     835           1 :                                   ? ValidationMessage.Type.ERROR
     836           1 :                                   : ValidationMessage.Type.OTHER))
     837           1 :                   .collect(toList());
     838           1 :           if (msgs.stream().anyMatch(ValidationMessage::isError)) {
     839           1 :             throw new CommitValidationException("invalid external IDs", msgs);
     840             :           }
     841           1 :           return msgs;
     842           0 :         } catch (IOException | ConfigInvalidException e) {
     843           0 :           throw new CommitValidationException("error validating external IDs", e);
     844             :         }
     845             :       }
     846         109 :       return Collections.emptyList();
     847             :     }
     848             :   }
     849             : 
     850             :   public static class AccountCommitValidator implements CommitValidationListener {
     851             :     private final GitRepositoryManager repoManager;
     852             :     private final AllUsersName allUsers;
     853             :     private final AccountValidator accountValidator;
     854             : 
     855             :     public AccountCommitValidator(
     856             :         GitRepositoryManager repoManager,
     857             :         AllUsersName allUsers,
     858         110 :         AccountValidator accountValidator) {
     859         110 :       this.repoManager = repoManager;
     860         110 :       this.allUsers = allUsers;
     861         110 :       this.accountValidator = accountValidator;
     862         110 :     }
     863             : 
     864             :     @Override
     865             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     866             :         throws CommitValidationException {
     867         109 :       if (!allUsers.equals(receiveEvent.project.getNameKey())) {
     868         107 :         return Collections.emptyList();
     869             :       }
     870             : 
     871           6 :       if (receiveEvent.command.getRefName().startsWith(MagicBranch.NEW_CHANGE)) {
     872             :         // no validation on push for review, will be checked on submit by
     873             :         // MergeValidators.AccountMergeValidator
     874           3 :         return Collections.emptyList();
     875             :       }
     876             : 
     877           5 :       Account.Id accountId = Account.Id.fromRef(receiveEvent.refName);
     878           5 :       if (accountId == null) {
     879           3 :         return Collections.emptyList();
     880             :       }
     881             : 
     882           2 :       try (Repository repo = repoManager.openRepository(allUsers)) {
     883           2 :         List<String> errorMessages =
     884           2 :             accountValidator.validate(
     885             :                 accountId,
     886             :                 repo,
     887             :                 receiveEvent.revWalk,
     888           2 :                 receiveEvent.command.getOldId(),
     889             :                 receiveEvent.commit);
     890           2 :         if (!errorMessages.isEmpty()) {
     891           1 :           throw new CommitValidationException(
     892             :               "invalid account configuration",
     893           1 :               errorMessages.stream()
     894           1 :                   .map(m -> new CommitValidationMessage(m, ValidationMessage.Type.ERROR))
     895           1 :                   .collect(toList()));
     896             :         }
     897           0 :       } catch (IOException e) {
     898           0 :         throw new CommitValidationException(
     899           0 :             String.format("Validating update for account %s failed", accountId.get()), e);
     900           2 :       }
     901           2 :       return Collections.emptyList();
     902             :     }
     903             :   }
     904             : 
     905             :   /** Rejects updates to group branches. */
     906             :   public static class GroupCommitValidator implements CommitValidationListener {
     907             :     private final AllUsersName allUsers;
     908             : 
     909         110 :     public GroupCommitValidator(AllUsersName allUsers) {
     910         110 :       this.allUsers = allUsers;
     911         110 :     }
     912             : 
     913             :     @Override
     914             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     915             :         throws CommitValidationException {
     916             :       // Groups are stored inside the 'All-Users' repository.
     917         109 :       if (!allUsers.equals(receiveEvent.project.getNameKey())) {
     918         107 :         return Collections.emptyList();
     919             :       }
     920             : 
     921           6 :       if (receiveEvent.command.getRefName().startsWith(MagicBranch.NEW_CHANGE)) {
     922             :         // no validation on push for review, will be checked on submit by
     923             :         // MergeValidators.GroupMergeValidator
     924           3 :         return Collections.emptyList();
     925             :       }
     926             : 
     927           5 :       if (RefNames.isGroupRef(receiveEvent.command.getRefName())) {
     928           1 :         throw new CommitValidationException("group update not allowed");
     929             :       }
     930           4 :       return Collections.emptyList();
     931             :     }
     932             :   }
     933             : 
     934             :   /** Rejects updates to projects that don't allow writes. */
     935             :   public static class ProjectStateValidationListener implements CommitValidationListener {
     936             :     private final ProjectState projectState;
     937             : 
     938         110 :     public ProjectStateValidationListener(ProjectState projectState) {
     939         110 :       this.projectState = projectState;
     940         110 :     }
     941             : 
     942             :     @Override
     943             :     public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
     944             :         throws CommitValidationException {
     945         110 :       if (projectState.statePermitsWrite()) {
     946         110 :         return Collections.emptyList();
     947             :       }
     948           0 :       throw new CommitValidationException("project state does not permit write");
     949             :     }
     950             :   }
     951             : 
     952             :   private static CommitValidationMessage invalidEmail(
     953             :       String type, PersonIdent who, IdentifiedUser currentUser, UrlFormatter urlFormatter) {
     954           1 :     StringBuilder sb = new StringBuilder();
     955             : 
     956           1 :     sb.append("email address ")
     957           1 :         .append(who.getEmailAddress())
     958           1 :         .append(" is not registered in your account, and you lack 'forge ")
     959           1 :         .append(type)
     960           1 :         .append("' permission.\n");
     961             : 
     962           1 :     if (currentUser.getEmailAddresses().isEmpty()) {
     963           0 :       sb.append("You have not registered any email addresses.\n");
     964             :     } else {
     965           1 :       sb.append("The following addresses are currently registered:\n");
     966           1 :       for (String address : currentUser.getEmailAddresses()) {
     967           1 :         sb.append("   ").append(address).append("\n");
     968           1 :       }
     969             :     }
     970             : 
     971           1 :     if (urlFormatter.getSettingsUrl("").isPresent()) {
     972           1 :       sb.append("To register an email address, visit:\n")
     973           1 :           .append(urlFormatter.getSettingsUrl("EmailAddresses").get())
     974           1 :           .append("\n\n");
     975             :     }
     976           1 :     return new CommitValidationMessage(sb.toString(), ValidationMessage.Type.ERROR);
     977             :   }
     978             : 
     979             :   /**
     980             :    * Get the Gerrit hostname.
     981             :    *
     982             :    * @return the hostname from the canonical URL if it is configured, otherwise whatever the OS says
     983             :    *     the hostname is.
     984             :    */
     985             :   private static String getGerritHost(String canonicalWebUrl) {
     986           0 :     if (canonicalWebUrl != null) {
     987             :       try {
     988           0 :         return new URL(canonicalWebUrl).getHost();
     989           0 :       } catch (MalformedURLException ignored) {
     990           0 :         logger.atWarning().log(
     991             :             "configured canonical web URL is invalid, using system default: %s",
     992           0 :             ignored.getMessage());
     993             :       }
     994             :     }
     995             : 
     996           0 :     return SystemReader.getInstance().getHostname();
     997             :   }
     998             : 
     999             :   private static void addError(String error, List<CommitValidationMessage> messages) {
    1000           4 :     messages.add(new CommitValidationMessage(error, ValidationMessage.Type.ERROR));
    1001           4 :   }
    1002             : }

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