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

com.google.gerrit.server.CommentsUtil Maven / Gradle / Ivy

There is a newer version: 3.11.1
Show newest version
// Copyright (C) 2014 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;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;

import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.update.ChangeContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;

/** Utility functions to manipulate Comments. */
@Singleton
public class CommentsUtil {
  public static final Ordering COMMENT_ORDER =
      new Ordering() {
        @Override
        public int compare(Comment c1, Comment c2) {
          return ComparisonChain.start()
              .compare(c1.key.filename, c2.key.filename)
              .compare(c1.key.patchSetId, c2.key.patchSetId)
              .compare(c1.side, c2.side)
              .compare(c1.lineNbr, c2.lineNbr)
              .compare(c1.writtenOn, c2.writtenOn)
              .result();
        }
      };

  public static final Ordering COMMENT_INFO_ORDER =
      new Ordering() {
        @Override
        public int compare(CommentInfo a, CommentInfo b) {
          return ComparisonChain.start()
              .compare(a.path, b.path, NULLS_FIRST)
              .compare(a.patchSet, b.patchSet, NULLS_FIRST)
              .compare(side(a), side(b))
              .compare(a.line, b.line, NULLS_FIRST)
              .compare(a.inReplyTo, b.inReplyTo, NULLS_FIRST)
              .compare(a.message, b.message)
              .compare(a.id, b.id)
              .result();
        }

        private int side(CommentInfo c) {
          return firstNonNull(c.side, Side.REVISION).ordinal();
        }
      };

  public static PatchSet.Id getCommentPsId(Change.Id changeId, Comment comment) {
    return PatchSet.id(changeId, comment.key.patchSetId);
  }

  public static String extractMessageId(@Nullable String tag) {
    if (tag == null || !tag.startsWith("mailMessageId=")) {
      return null;
    }
    return tag.substring("mailMessageId=".length());
  }

  private static final Ordering> NULLS_FIRST = Ordering.natural().nullsFirst();

  private final GitRepositoryManager repoManager;
  private final AllUsersName allUsers;
  private final String serverId;

  @Inject
  CommentsUtil(
      GitRepositoryManager repoManager, AllUsersName allUsers, @GerritServerId String serverId) {
    this.repoManager = repoManager;
    this.allUsers = allUsers;
    this.serverId = serverId;
  }

  public Comment newComment(
      ChangeContext ctx,
      String path,
      PatchSet.Id psId,
      short side,
      String message,
      @Nullable Boolean unresolved,
      @Nullable String parentUuid)
      throws UnprocessableEntityException {
    if (unresolved == null) {
      if (parentUuid == null) {
        // Default to false if comment is not descended from another.
        unresolved = false;
      } else {
        // Inherit unresolved value from inReplyTo comment if not specified.
        Comment.Key key = new Comment.Key(parentUuid, path, psId.get());
        Optional parent = getPublished(ctx.getNotes(), key);
        if (!parent.isPresent()) {
          throw new UnprocessableEntityException("Invalid parentUuid supplied for comment");
        }
        unresolved = parent.get().unresolved;
      }
    }
    Comment c =
        new Comment(
            new Comment.Key(ChangeUtil.messageUuid(), path, psId.get()),
            ctx.getUser().getAccountId(),
            ctx.getWhen(),
            side,
            message,
            serverId,
            unresolved);
    c.parentUuid = parentUuid;
    ctx.getUser().updateRealAccountId(c::setRealAuthor);
    return c;
  }

  public RobotComment newRobotComment(
      ChangeContext ctx,
      String path,
      PatchSet.Id psId,
      short side,
      String message,
      String robotId,
      String robotRunId) {
    RobotComment c =
        new RobotComment(
            new Comment.Key(ChangeUtil.messageUuid(), path, psId.get()),
            ctx.getUser().getAccountId(),
            ctx.getWhen(),
            side,
            message,
            serverId,
            robotId,
            robotRunId);
    ctx.getUser().updateRealAccountId(c::setRealAuthor);
    return c;
  }

  public Optional getPublished(ChangeNotes notes, Comment.Key key) {
    return publishedByChange(notes).stream().filter(c -> key.equals(c.key)).findFirst();
  }

  public Optional getDraft(ChangeNotes notes, IdentifiedUser user, Comment.Key key) {
    return draftByChangeAuthor(notes, user.getAccountId()).stream()
        .filter(c -> key.equals(c.key))
        .findFirst();
  }

  public List publishedByChange(ChangeNotes notes) {
    notes.load();
    return sort(Lists.newArrayList(notes.getComments().values()));
  }

  public List robotCommentsByChange(ChangeNotes notes) {
    notes.load();
    return sort(Lists.newArrayList(notes.getRobotComments().values()));
  }

  public List draftByChange(ChangeNotes notes) {
    List comments = new ArrayList<>();
    for (Ref ref : getDraftRefs(notes.getChangeId())) {
      Account.Id account = Account.Id.fromRefSuffix(ref.getName());
      if (account != null) {
        comments.addAll(draftByChangeAuthor(notes, account));
      }
    }
    return sort(comments);
  }

  public List byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
    List comments = new ArrayList<>();
    comments.addAll(publishedByPatchSet(notes, psId));

    for (Ref ref : getDraftRefs(notes.getChangeId())) {
      Account.Id account = Account.Id.fromRefSuffix(ref.getName());
      if (account != null) {
        comments.addAll(draftByPatchSetAuthor(psId, account, notes));
      }
    }
    return sort(comments);
  }

  public List publishedByChangeFile(ChangeNotes notes, String file) {
    return commentsOnFile(notes.load().getComments().values(), file);
  }

  public List publishedByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
    return removeCommentsOnAncestorOfCommitMessage(
        commentsOnPatchSet(notes.load().getComments().values(), psId));
  }

  public List robotCommentsByPatchSet(ChangeNotes notes, PatchSet.Id psId) {
    return commentsOnPatchSet(notes.load().getRobotComments().values(), psId);
  }

  /**
   * This method populates the "changeMessageId" field of the comments parameter based on timestamp
   * matching. The comments objects will be modified.
   *
   * 

Each comment will be matched to the nearest next change message in timestamp * * @param comments the list of comments * @param changeMessages list of change messages */ public static void linkCommentsToChangeMessages( List comments, List changeMessages) { ArrayList sortedChangeMessages = changeMessages.stream() .sorted(comparing(ChangeMessage::getWrittenOn)) .collect(toCollection(ArrayList::new)); ArrayList sortedCommentInfos = comments.stream().sorted(comparing(c -> c.updated)).collect(toCollection(ArrayList::new)); int cmItr = 0; for (CommentInfo comment : sortedCommentInfos) { // Keep advancing the change message pointer until we associate the comment to the next change // message in timestamp while (cmItr < sortedChangeMessages.size()) { ChangeMessage cm = sortedChangeMessages.get(cmItr); if (isAfter(comment, cm) || skipChangeMessage(cm)) { cmItr += 1; } else { break; } } if (cmItr < changeMessages.size()) { comment.changeMessageId = sortedChangeMessages.get(cmItr).getKey().uuid(); } } } private static boolean skipChangeMessage(ChangeMessage cm) { return ChangeMessagesUtil.isAutogenerated(cm.getTag()); } private static boolean isAfter(CommentInfo c, ChangeMessage cm) { return c.updated.after(cm.getWrittenOn()); } /** * For the commit message the A side in a diff view is always empty when a comparison against an * ancestor is done, so there can't be any comments on this ancestor. However earlier we showed * the auto-merge commit message on side A when for a merge commit a comparison against the * auto-merge was done. From that time there may still be comments on the auto-merge commit * message and those we want to filter out. */ private List removeCommentsOnAncestorOfCommitMessage(List list) { return list.stream() .filter(c -> c.side != 0 || !Patch.COMMIT_MSG.equals(c.key.filename)) .collect(toList()); } public List draftByPatchSetAuthor( PatchSet.Id psId, Account.Id author, ChangeNotes notes) { return commentsOnPatchSet(notes.load().getDraftComments(author).values(), psId); } public List draftByChangeFileAuthor(ChangeNotes notes, String file, Account.Id author) { return commentsOnFile(notes.load().getDraftComments(author).values(), file); } public List draftByChangeAuthor(ChangeNotes notes, Account.Id author) { List comments = new ArrayList<>(); comments.addAll(notes.getDraftComments(author).values()); return sort(comments); } public void putComments(ChangeUpdate update, Comment.Status status, Iterable comments) { for (Comment c : comments) { update.putComment(status, c); } } public void putRobotComments(ChangeUpdate update, Iterable comments) { for (RobotComment c : comments) { update.putRobotComment(c); } } public void deleteComments(ChangeUpdate update, Iterable comments) { for (Comment c : comments) { update.deleteComment(c); } } public void deleteCommentByRewritingHistory( ChangeUpdate update, Comment.Key commentKey, String newMessage) { update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage); } private static List commentsOnFile(Collection allComments, String file) { List result = new ArrayList<>(allComments.size()); for (Comment c : allComments) { String currentFilename = c.key.filename; if (currentFilename.equals(file)) { result.add(c); } } return sort(result); } private static List commentsOnPatchSet( Collection allComments, PatchSet.Id psId) { List result = new ArrayList<>(allComments.size()); for (T c : allComments) { if (c.key.patchSetId == psId.get()) { result.add(c); } } return sort(result); } public static void setCommentCommitId(Comment c, PatchListCache cache, Change change, PatchSet ps) throws PatchListNotAvailableException { checkArgument( c.key.patchSetId == ps.id().get(), "cannot set commit ID for patch set %s on comment %s", ps.id(), c); if (c.getCommitId() == null) { if (Side.fromShort(c.side) == Side.PARENT) { if (c.side < 0) { c.setCommitId(cache.getOldId(change, ps, -c.side)); } else { c.setCommitId(cache.getOldId(change, ps, null)); } } else { c.setCommitId(ps.commitId()); } } } /** * Get NoteDb draft refs for a change. * *

Works if NoteDb is not enabled, but the results are not meaningful. * *

This is just a simple ref scan, so the results may potentially include refs for zombie draft * comments. A zombie draft is one which has been published but the write to delete the draft ref * from All-Users failed. * * @param changeId change ID. * @return raw refs from All-Users repo. */ public Collection getDraftRefs(Change.Id changeId) { try (Repository repo = repoManager.openRepository(allUsers)) { return getDraftRefs(repo, changeId); } catch (IOException e) { throw new StorageException(e); } } private Collection getDraftRefs(Repository repo, Change.Id changeId) throws IOException { return repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(changeId)); } private static List sort(List comments) { comments.sort(COMMENT_ORDER); return comments; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy