| // Copyright (C) 2018 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.server.git.receive; | |
| import static com.google.gerrit.git.ObjectIds.abbreviateName; | |
| import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; | |
| import com.google.auto.value.AutoValue; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableListMultimap; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.entities.BranchNameKey; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.server.IdentifiedUser; | |
| import com.google.gerrit.server.events.CommitReceivedEvent; | |
| import com.google.gerrit.server.git.validators.CommitValidationException; | |
| import com.google.gerrit.server.git.validators.CommitValidationMessage; | |
| import com.google.gerrit.server.git.validators.CommitValidators; | |
| import com.google.gerrit.server.logging.TraceContext; | |
| import com.google.gerrit.server.logging.TraceContext.TraceTimer; | |
| import com.google.gerrit.server.permissions.PermissionBackend; | |
| import com.google.gerrit.server.project.ProjectState; | |
| import com.google.gerrit.server.ssh.SshInfo; | |
| import com.google.inject.Inject; | |
| import com.google.inject.assistedinject.Assisted; | |
| import java.io.IOException; | |
| import org.eclipse.jgit.lib.Config; | |
| import org.eclipse.jgit.lib.ObjectReader; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.notes.NoteMap; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| import org.eclipse.jgit.transport.ReceiveCommand; | |
| /** Validates single commits for a branch. */ | |
| public class BranchCommitValidator { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| private final CommitValidators.Factory commitValidatorsFactory; | |
| private final IdentifiedUser user; | |
| private final PermissionBackend.ForProject permissions; | |
| private final Project project; | |
| private final BranchNameKey branch; | |
| private final SshInfo sshInfo; | |
| interface Factory { | |
| BranchCommitValidator create( | |
| ProjectState projectState, BranchNameKey branch, IdentifiedUser user); | |
| } | |
| /** A boolean validation status and a list of additional messages. */ | |
| @AutoValue | |
| abstract static class Result { | |
| static Result create(boolean isValid, ImmutableList<CommitValidationMessage> messages) { | |
| return new AutoValue_BranchCommitValidator_Result(isValid, messages); | |
| } | |
| /** Whether the commit is valid. */ | |
| abstract boolean isValid(); | |
| /** | |
| * A list of messages related to the validation. Messages may be present regardless of the | |
| * {@link #isValid()} status. | |
| */ | |
| abstract ImmutableList<CommitValidationMessage> messages(); | |
| } | |
| @Inject | |
| BranchCommitValidator( | |
| CommitValidators.Factory commitValidatorsFactory, | |
| PermissionBackend permissionBackend, | |
| SshInfo sshInfo, | |
| @Assisted ProjectState projectState, | |
| @Assisted BranchNameKey branch, | |
| @Assisted IdentifiedUser user) { | |
| this.sshInfo = sshInfo; | |
| this.user = user; | |
| this.branch = branch; | |
| this.commitValidatorsFactory = commitValidatorsFactory; | |
| project = projectState.getProject(); | |
| permissions = permissionBackend.user(user).project(project.getNameKey()); | |
| } | |
| /** | |
| * Validates a single commit. If the commit does not validate, the command is rejected. | |
| * | |
| * @param repository the repository | |
| * @param objectReader the object reader to use. | |
| * @param cmd the ReceiveCommand executing the push. | |
| * @param commit the commit being validated. | |
| * @param isMerged whether this is a merge commit created by magicBranch --merge option | |
| * @param change the change for which this is a new patchset. | |
| * @return The validation {@link Result}. | |
| */ | |
| Result validateCommit( | |
| Repository repository, | |
| ObjectReader objectReader, | |
| ReceiveCommand cmd, | |
| RevCommit commit, | |
| ImmutableListMultimap<String, String> pushOptions, | |
| boolean isMerged, | |
| NoteMap rejectCommits, | |
| @Nullable Change change) | |
| throws IOException { | |
| return validateCommit( | |
| repository, objectReader, cmd, commit, pushOptions, isMerged, rejectCommits, change, false); | |
| } | |
| /** | |
| * Validates a single commit. If the commit does not validate, the command is rejected. | |
| * | |
| * @param repository the repository | |
| * @param objectReader the object reader to use. | |
| * @param cmd the ReceiveCommand executing the push. | |
| * @param commit the commit being validated. | |
| * @param isMerged whether this is a merge commit created by magicBranch --merge option | |
| * @param change the change for which this is a new patchset. | |
| * @param skipValidation whether 'skip-validation' was requested. | |
| * @return The validation {@link Result}. | |
| */ | |
| Result validateCommit( | |
| Repository repository, | |
| ObjectReader objectReader, | |
| ReceiveCommand cmd, | |
| RevCommit commit, | |
| ImmutableListMultimap<String, String> pushOptions, | |
| boolean isMerged, | |
| NoteMap rejectCommits, | |
| @Nullable Change change, | |
| boolean skipValidation) | |
| throws IOException { | |
| try (TraceTimer traceTimer = TraceContext.newTimer("BranchCommitValidator#validateCommit")) { | |
| ImmutableList.Builder<CommitValidationMessage> messages = new ImmutableList.Builder<>(); | |
| try (CommitReceivedEvent receiveEvent = | |
| new CommitReceivedEvent( | |
| cmd, | |
| project, | |
| branch.branch(), | |
| pushOptions, | |
| new Config(repository.getConfig()), | |
| objectReader, | |
| commit, | |
| user)) { | |
| CommitValidators validators; | |
| if (isMerged) { | |
| validators = | |
| commitValidatorsFactory.forMergedCommits( | |
| permissions, branch, user.asIdentifiedUser()); | |
| } else { | |
| validators = | |
| commitValidatorsFactory.forReceiveCommits( | |
| permissions, | |
| branch, | |
| user.asIdentifiedUser(), | |
| sshInfo, | |
| rejectCommits, | |
| receiveEvent.revWalk, | |
| change, | |
| skipValidation); | |
| } | |
| for (CommitValidationMessage m : validators.validate(receiveEvent)) { | |
| messages.add( | |
| new CommitValidationMessage( | |
| messageForCommit(commit, m.getMessage(), objectReader), m.getType())); | |
| } | |
| } catch (CommitValidationException e) { | |
| logger.atFine().log("Commit validation failed on %s", commit.name()); | |
| for (CommitValidationMessage m : e.getMessages()) { | |
| // The non-error messages may contain background explanation for the | |
| // fatal error, so have to preserve all messages. | |
| messages.add( | |
| new CommitValidationMessage( | |
| messageForCommit(commit, m.getMessage(), objectReader), m.getType())); | |
| } | |
| cmd.setResult( | |
| REJECTED_OTHER_REASON, messageForCommit(commit, e.getMessage(), objectReader)); | |
| return Result.create(false, messages.build()); | |
| } | |
| return Result.create(true, messages.build()); | |
| } | |
| } | |
| private String messageForCommit(RevCommit c, String msg, ObjectReader objectReader) | |
| throws IOException { | |
| return String.format("commit %s: %s", abbreviateName(c, objectReader), msg); | |
| } | |
| } |