com.google.gerrit.server.git.CommitUtil Maven / Gradle / Ivy
// Copyright (C) 2017 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;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gerrit.server.mail.EmailFactories.CHANGE_REVERTED;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommonConverters;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ValidationOptionsUtil;
import com.google.gerrit.server.extensions.events.ChangeReverted;
import com.google.gerrit.server.mail.EmailFactories;
import com.google.gerrit.server.mail.send.ChangeEmail;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.OutgoingEmail;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.PostUpdateContext;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.CommitMessageUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.ChangeIdUtil;
/** Static utilities for working with {@link RevCommit}s. */
@Singleton
public class CommitUtil {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final GitRepositoryManager repoManager;
private final Provider serverIdent;
private final Sequences seq;
private final ApprovalsUtil approvalsUtil;
private final ChangeInserter.Factory changeInserterFactory;
private final NotifyResolver notifyResolver;
private final EmailFactories emailFactories;
private final ChangeMessagesUtil cmUtil;
private final ChangeNotes.Factory changeNotesFactory;
private final ChangeReverted changeReverted;
private final BatchUpdate.Factory updateFactory;
private final MessageIdGenerator messageIdGenerator;
@Inject
CommitUtil(
GitRepositoryManager repoManager,
@GerritPersonIdent Provider serverIdent,
Sequences seq,
ApprovalsUtil approvalsUtil,
ChangeInserter.Factory changeInserterFactory,
NotifyResolver notifyResolver,
EmailFactories emailFactories,
ChangeMessagesUtil cmUtil,
ChangeNotes.Factory changeNotesFactory,
ChangeReverted changeReverted,
BatchUpdate.Factory updateFactory,
MessageIdGenerator messageIdGenerator) {
this.repoManager = repoManager;
this.serverIdent = serverIdent;
this.seq = seq;
this.approvalsUtil = approvalsUtil;
this.changeInserterFactory = changeInserterFactory;
this.notifyResolver = notifyResolver;
this.emailFactories = emailFactories;
this.cmUtil = cmUtil;
this.changeNotesFactory = changeNotesFactory;
this.changeReverted = changeReverted;
this.updateFactory = updateFactory;
this.messageIdGenerator = messageIdGenerator;
}
public static CommitInfo toCommitInfo(RevCommit commit) throws IOException {
return toCommitInfo(commit, null);
}
public static CommitInfo toCommitInfo(RevCommit commit, @Nullable RevWalk walk)
throws IOException {
CommitInfo info = new CommitInfo();
info.commit = commit.getName();
info.author = CommonConverters.toGitPerson(commit.getAuthorIdent());
info.committer = CommonConverters.toGitPerson(commit.getCommitterIdent());
info.subject = commit.getShortMessage();
info.message = commit.getFullMessage();
info.parents = new ArrayList<>(commit.getParentCount());
for (int i = 0; i < commit.getParentCount(); i++) {
RevCommit p = walk == null ? commit.getParent(i) : walk.parseCommit(commit.getParent(i));
CommitInfo parentInfo = new CommitInfo();
parentInfo.commit = p.getName();
parentInfo.subject = p.getShortMessage();
info.parents.add(parentInfo);
}
return info;
}
/**
* Allows creating a revert change.
*
* @param notes ChangeNotes of the change being reverted.
* @param user Current User performing the revert.
* @param input the RevertInput entity for conducting the revert.
* @param timestamp timestamp for the created change.
* @return ObjectId that represents the newly created commit.
*/
public Change.Id createRevertChange(
ChangeNotes notes, CurrentUser user, RevertInput input, Instant timestamp)
throws RestApiException, UpdateException, ConfigInvalidException, IOException {
String message = Strings.emptyToNull(input.message);
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
try (Repository git = repoManager.openRepository(notes.getProjectName());
ObjectInserter oi = git.newObjectInserter();
ObjectReader reader = oi.newReader();
RevWalk revWalk = new RevWalk(reader)) {
ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
ObjectId revCommit =
createRevertCommit(message, notes, user, timestamp, oi, revWalk, generatedChangeId);
return createRevertChangeFromCommit(
revCommit, input, notes, user, generatedChangeId, timestamp, oi, revWalk, git);
} catch (RepositoryNotFoundException e) {
throw new ResourceNotFoundException(notes.getChangeId().toString(), e);
}
}
}
/**
* Wrapper function for creating a revert Commit.
*
* @param message Commit message for the revert commit.
* @param notes ChangeNotes of the change being reverted.
* @param user Current User performing the revert.
* @param ts Timestamp of creation for the commit.
* @return ObjectId that represents the newly created commit.
*/
public ObjectId createRevertCommit(
String message, ChangeNotes notes, CurrentUser user, Instant ts)
throws RestApiException, IOException {
try (Repository git = repoManager.openRepository(notes.getProjectName());
ObjectInserter oi = git.newObjectInserter();
ObjectReader reader = oi.newReader();
RevWalk revWalk = new RevWalk(reader)) {
return createRevertCommit(message, notes, user, ts, oi, revWalk, null);
} catch (RepositoryNotFoundException e) {
throw new ResourceNotFoundException(notes.getProjectName().toString(), e);
}
}
/**
* Creates a commit with the specified tree ID.
*
* @param oi ObjectInserter for inserting the newly created commit.
* @param authorIdent of the new commit
* @param committerIdent of the new commit
* @param parents of the new commit. Can be empty.
* @param commitMessage for the new commit.
* @param treeId of the content for the new commit.
* @return the newly created commit.
* @throws IOException if fails to insert the commit.
*/
public static ObjectId createCommitWithTree(
ObjectInserter oi,
PersonIdent authorIdent,
PersonIdent committerIdent,
List parents,
String commitMessage,
ObjectId treeId)
throws IOException {
logger.atFine().log("Creating commit with tree: %s", treeId.getName());
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(treeId);
commit.setParentIds(parents.stream().map(RevCommit::getId).collect(Collectors.toList()));
commit.setAuthor(authorIdent);
commit.setCommitter(committerIdent);
commit.setMessage(commitMessage);
ObjectId id = oi.insert(commit);
oi.flush();
return id;
}
/**
* Creates a revert commit.
*
* @param message Commit message for the revert commit.
* @param notes ChangeNotes of the change being reverted.
* @param user Current User performing the revert.
* @param ts Timestamp of creation for the commit.
* @param oi ObjectInserter for inserting the newly created commit.
* @param revWalk Used for parsing the original commit.
* @param generatedChangeId The changeId for the commit message, can be null since it is not
* needed for commits, only for changes.
* @return ObjectId that represents the newly created commit.
* @throws ResourceConflictException Can't revert the initial commit.
* @throws IOException Thrown in case of I/O errors.
*/
private ObjectId createRevertCommit(
String message,
ChangeNotes notes,
CurrentUser user,
Instant ts,
ObjectInserter oi,
RevWalk revWalk,
@Nullable ObjectId generatedChangeId)
throws ResourceConflictException, IOException {
PatchSet patch = notes.getCurrentPatchSet();
RevCommit commitToRevert = revWalk.parseCommit(patch.commitId());
if (commitToRevert.getParentCount() == 0) {
throw new ResourceConflictException("Cannot revert initial commit");
}
PersonIdent committerIdent = serverIdent.get();
PersonIdent authorIdent =
user.asIdentifiedUser().newCommitterIdent(ts, committerIdent.getZoneId());
RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
revWalk.parseHeaders(parentToCommitToRevert);
Change changeToRevert = notes.getChange();
String subject = changeToRevert.getSubject();
if (subject.length() > 63) {
subject = subject.substring(0, 59) + "...";
}
if (message == null) {
message =
MessageFormat.format(
ChangeMessages.get().revertChangeDefaultMessage, subject, patch.commitId().name());
}
if (generatedChangeId != null) {
message = ChangeIdUtil.insertId(message, generatedChangeId, true);
}
return createCommitWithTree(
oi,
authorIdent,
committerIdent,
ImmutableList.of(commitToRevert),
message,
parentToCommitToRevert.getTree());
}
private Change.Id createRevertChangeFromCommit(
ObjectId revertCommitId,
RevertInput input,
ChangeNotes notes,
CurrentUser user,
@Nullable ObjectId generatedChangeId,
Instant ts,
ObjectInserter oi,
RevWalk revWalk,
Repository git)
throws IOException, RestApiException, UpdateException, ConfigInvalidException {
RevCommit revertCommit = revWalk.parseCommit(revertCommitId);
Change.Id changeId = Change.id(seq.nextChangeId());
if (input.workInProgress) {
input.notify = firstNonNull(input.notify, NotifyHandling.NONE);
}
NotifyResolver.Result notify =
notifyResolver.resolve(firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
Change changeToRevert = notes.getChange();
ChangeInserter ins =
changeInserterFactory
.create(changeId, revertCommit, changeToRevert.getDest().branch())
.setTopic(input.topic == null ? changeToRevert.getTopic() : input.topic.trim());
ins.setMessage("Uploaded patch set 1.");
ins.setValidationOptions(
ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes);
Set reviewers = new HashSet<>();
reviewers.add(changeToRevert.getOwner());
reviewers.addAll(reviewerSet.byState(ReviewerStateInternal.REVIEWER));
reviewers.remove(user.getAccountId());
Set ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC));
ccs.remove(user.getAccountId());
ins.setReviewersAndCcsIgnoreVisibility(reviewers, ccs);
ins.setRevertOf(notes.getChangeId());
ins.setWorkInProgress(input.workInProgress);
try (BatchUpdate bu = updateFactory.create(notes.getProjectName(), user, ts)) {
bu.setRepository(git, revWalk, oi);
bu.setNotify(notify);
bu.insertChange(ins);
if (!input.workInProgress) {
addChangeRevertedNotificationOps(
bu, changeToRevert.getId(), changeId, generatedChangeId.name());
}
bu.execute();
}
return changeId;
}
/**
* Notify the owners of a change that their change is being reverted.
*
* @param bu to append the notification actions to.
* @param revertedChangeId to be notified.
* @param revertingChangeId to notify about.
* @param revertingChangeKey to notify about.
*/
public void addChangeRevertedNotificationOps(
BatchUpdate bu,
Change.Id revertedChangeId,
Change.Id revertingChangeId,
String revertingChangeKey) {
bu.addOp(revertingChangeId, new ChangeRevertedNotifyOp(revertedChangeId, revertingChangeId));
bu.addOp(revertedChangeId, new PostRevertedMessageOp(revertingChangeKey));
}
private class ChangeRevertedNotifyOp implements BatchUpdateOp {
private final Change.Id revertedChangeId;
private final Change.Id revertingChangeId;
ChangeRevertedNotifyOp(Change.Id revertedChangeId, Change.Id revertingChangeId) {
this.revertedChangeId = revertedChangeId;
this.revertingChangeId = revertingChangeId;
}
@Override
public void postUpdate(PostUpdateContext ctx) throws Exception {
ChangeData revertedChange =
ctx.getChangeData(changeNotesFactory.createChecked(ctx.getProject(), revertedChangeId));
ChangeData revertingChange =
ctx.getChangeData(changeNotesFactory.createChecked(ctx.getProject(), revertingChangeId));
changeReverted.fire(revertedChange, revertingChange, ctx.getWhen());
try {
ChangeEmail changeEmail =
emailFactories.createChangeEmail(
ctx.getProject(),
revertedChange.getId(),
emailFactories.createRevertedChangeEmail());
OutgoingEmail outgoingEmail =
emailFactories.createOutgoingEmail(CHANGE_REVERTED, changeEmail);
outgoingEmail.setFrom(ctx.getAccountId());
outgoingEmail.setNotify(ctx.getNotify(revertedChangeId));
outgoingEmail.setMessageId(
messageIdGenerator.fromChangeUpdate(
ctx.getRepoView(), revertedChange.currentPatchSet().id()));
outgoingEmail.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log(
"Cannot send email for revert change %s", revertedChangeId);
}
}
}
private class PostRevertedMessageOp implements BatchUpdateOp {
private final String revertingChangeKey;
PostRevertedMessageOp(String revertingChangeKey) {
this.revertingChangeKey = revertingChangeKey;
}
@Override
public boolean updateChange(ChangeContext ctx) {
cmUtil.setChangeMessage(
ctx,
"Created a revert of this change as I" + revertingChangeKey,
ChangeMessagesUtil.TAG_REVERT);
return true;
}
}
/**
* Returns the parent commit for a new commit.
*
* If {@code baseSha1} is provided, the method verifies it can be used as a base. If {@code
* baseSha1} is not provided the tip of the {@code destRef} is returned.
*
* @param project The name of the project.
* @param changeQuery Used for looking up the base commit.
* @param revWalk Used for parsing the base commit.
* @param destRef The destination branch.
* @param baseSha1 The hash of the base commit. Nullable.
* @return the base commit. Either the commit matching the provided hash, or the direct parent if
* a hash was not provided.
* @throws IOException if the branch reference cannot be parsed.
* @throws RestApiException if the base commit cannot be fetched.
*/
public static RevCommit getBaseCommit(
String project,
InternalChangeQuery changeQuery,
RevWalk revWalk,
Ref destRef,
@Nullable String baseSha1)
throws IOException, RestApiException {
RevCommit destRefTip = revWalk.parseCommit(destRef.getObjectId());
// The tip commit of the destination ref is the default base for the newly created change.
if (Strings.isNullOrEmpty(baseSha1)) {
return destRefTip;
}
ObjectId baseObjectId;
try {
baseObjectId = ObjectId.fromString(baseSha1);
} catch (InvalidObjectIdException e) {
throw new BadRequestException(
String.format("Base %s doesn't represent a valid SHA-1", baseSha1), e);
}
RevCommit baseCommit;
try {
baseCommit = revWalk.parseCommit(baseObjectId);
} catch (MissingObjectException e) {
throw new UnprocessableEntityException(
String.format("Base %s doesn't exist", baseObjectId.name()), e);
}
changeQuery.enforceVisibility(true);
List changeDatas = changeQuery.byBranchCommit(project, destRef.getName(), baseSha1);
if (changeDatas.isEmpty()) {
if (revWalk.isMergedInto(baseCommit, destRefTip)) {
// The base commit is a merged commit with no change associated.
return baseCommit;
}
throw new UnprocessableEntityException(
String.format("Commit %s does not exist on branch %s", baseSha1, destRef.getName()));
} else if (changeDatas.size() != 1) {
throw new ResourceConflictException("Multiple changes found for commit " + baseSha1);
}
Change change = changeDatas.get(0).change();
if (!change.isAbandoned()) {
// The base commit is a valid change revision.
return baseCommit;
}
throw new ResourceConflictException(
String.format(
"Change %s with commit %s is %s",
change.getChangeId(), baseSha1, ChangeUtil.status(change)));
}
}