blob: f680b7bb8e66d46507f70f567dbc28d93c5e00d6 [file] [log] [blame]
// 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);
}
}