All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.git.CommitUtil Maven / Gradle / Ivy

The newest version!
// 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.getWorkInProgress()) {
      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.getWorkInProgress());

    try (BatchUpdate bu = updateFactory.create(notes.getProjectName(), user, ts)) {
      bu.setRepository(git, revWalk, oi);
      bu.setNotify(notify);
      bu.insertChange(ins);
      if (!input.getWorkInProgress()) {
        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))); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy