LCOV - code coverage report
Current view: top level - server/comment - CommentContextLoader.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 88 93 94.6 %
Date: 2022-11-19 15:00:39 Functions: 16 17 94.1 %

          Line data    Source code
       1             : // Copyright (C) 2020 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.comment;
      16             : 
      17             : import static com.google.gerrit.entities.Patch.COMMIT_MSG;
      18             : import static com.google.gerrit.entities.Patch.MERGE_LIST;
      19             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      20             : import static java.util.stream.Collectors.groupingBy;
      21             : 
      22             : import com.google.auto.value.AutoValue;
      23             : import com.google.common.collect.ImmutableMap;
      24             : import com.google.common.collect.Iterables;
      25             : import com.google.common.flogger.FluentLogger;
      26             : import com.google.gerrit.common.Nullable;
      27             : import com.google.gerrit.common.data.PatchScript;
      28             : import com.google.gerrit.entities.Comment;
      29             : import com.google.gerrit.entities.CommentContext;
      30             : import com.google.gerrit.entities.Project;
      31             : import com.google.gerrit.extensions.common.ContextLineInfo;
      32             : import com.google.gerrit.server.change.FileContentUtil;
      33             : import com.google.gerrit.server.git.GitRepositoryManager;
      34             : import com.google.gerrit.server.mime.FileTypeRegistry;
      35             : import com.google.gerrit.server.patch.ComparisonType;
      36             : import com.google.gerrit.server.patch.SrcContentResolver;
      37             : import com.google.gerrit.server.patch.Text;
      38             : import com.google.gerrit.server.project.ProjectCache;
      39             : import com.google.gerrit.server.project.ProjectState;
      40             : import com.google.inject.Inject;
      41             : import com.google.inject.assistedinject.Assisted;
      42             : import eu.medsea.mimeutil.MimeType;
      43             : import eu.medsea.mimeutil.MimeUtil2;
      44             : import java.io.IOException;
      45             : import java.util.Collection;
      46             : import java.util.List;
      47             : import java.util.Map;
      48             : import java.util.Optional;
      49             : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
      50             : import org.eclipse.jgit.errors.MissingObjectException;
      51             : import org.eclipse.jgit.lib.ObjectId;
      52             : import org.eclipse.jgit.lib.ObjectReader;
      53             : import org.eclipse.jgit.lib.Repository;
      54             : import org.eclipse.jgit.revwalk.RevCommit;
      55             : import org.eclipse.jgit.revwalk.RevWalk;
      56             : import org.eclipse.jgit.treewalk.TreeWalk;
      57             : 
      58             : /**
      59             :  * Computes the list of {@link ContextLineInfo} for a given comment, that is, the lines of the
      60             :  * source file surrounding and including the area where the comment was written.
      61             :  */
      62             : public class CommentContextLoader {
      63           1 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      64             : 
      65             :   private final FileTypeRegistry registry;
      66             :   private final GitRepositoryManager repoManager;
      67             :   private final Project.NameKey project;
      68             :   private final ProjectState projectState;
      69             : 
      70             :   public interface Factory {
      71             :     CommentContextLoader create(Project.NameKey project);
      72             :   }
      73             : 
      74             :   @Inject
      75             :   CommentContextLoader(
      76             :       FileTypeRegistry registry,
      77             :       GitRepositoryManager repoManager,
      78             :       ProjectCache projectCache,
      79           1 :       @Assisted Project.NameKey project) {
      80           1 :     this.registry = registry;
      81           1 :     this.repoManager = repoManager;
      82           1 :     this.project = project;
      83           1 :     projectState = projectCache.get(project).orElseThrow(illegalState(project));
      84           1 :   }
      85             : 
      86             :   /**
      87             :    * Load the comment context for multiple contextInputs at once. This method will open the
      88             :    * repository and read the source files for all necessary contextInputs' file paths.
      89             :    *
      90             :    * @param contextInputs a list of contextInputs.
      91             :    * @return a Map where all entries consist of the input contextInputs and the values are their
      92             :    *     corresponding {@link CommentContext}.
      93             :    */
      94             :   public Map<ContextInput, CommentContext> getContext(Collection<ContextInput> contextInputs)
      95             :       throws IOException {
      96           1 :     ImmutableMap.Builder<ContextInput, CommentContext> result =
      97           1 :         ImmutableMap.builderWithExpectedSize(Iterables.size(contextInputs));
      98             : 
      99             :     // Group contextInputs by commit ID so that each commit is parsed only once
     100           1 :     Map<ObjectId, List<ContextInput>> commentsByCommitId =
     101           1 :         contextInputs.stream().collect(groupingBy(ContextInput::commitId));
     102             : 
     103           1 :     try (Repository repo = repoManager.openRepository(project);
     104           1 :         RevWalk rw = new RevWalk(repo)) {
     105           1 :       for (ObjectId commitId : commentsByCommitId.keySet()) {
     106             :         RevCommit commit;
     107             :         try {
     108           1 :           commit = rw.parseCommit(commitId);
     109           1 :         } catch (IncorrectObjectTypeException | MissingObjectException e) {
     110           1 :           logger.atWarning().log("Commit %s is missing or has an incorrect object type", commitId);
     111           1 :           commentsByCommitId
     112           1 :               .get(commitId)
     113           1 :               .forEach(contextInput -> result.put(contextInput, CommentContext.empty()));
     114           1 :           continue;
     115           1 :         }
     116           1 :         for (ContextInput contextInput : commentsByCommitId.get(commitId)) {
     117           1 :           Optional<Range> range = getStartAndEndLines(contextInput);
     118           1 :           if (!range.isPresent()) {
     119           1 :             result.put(contextInput, CommentContext.empty());
     120           1 :             continue;
     121             :           }
     122           1 :           String filePath = contextInput.filePath();
     123           1 :           switch (filePath) {
     124             :             case COMMIT_MSG:
     125           1 :               result.put(
     126             :                   contextInput,
     127           1 :                   getContextForCommitMessage(
     128           1 :                       rw.getObjectReader(), commit, range.get(), contextInput.contextPadding()));
     129           1 :               break;
     130             :             case MERGE_LIST:
     131           1 :               result.put(
     132             :                   contextInput,
     133           1 :                   getContextForMergeList(
     134           1 :                       rw.getObjectReader(), commit, range.get(), contextInput.contextPadding()));
     135           1 :               break;
     136             :             default:
     137           1 :               result.put(
     138             :                   contextInput,
     139           1 :                   getContextForFilePath(
     140           1 :                       repo, rw, commit, filePath, range.get(), contextInput.contextPadding()));
     141             :           }
     142           1 :         }
     143           1 :       }
     144           1 :       return result.build();
     145             :     }
     146             :   }
     147             : 
     148             :   private CommentContext getContextForCommitMessage(
     149             :       ObjectReader reader, RevCommit commit, Range commentRange, int contextPadding)
     150             :       throws IOException {
     151           1 :     Text text = Text.forCommit(reader, commit);
     152           1 :     return createContext(
     153             :         text, commentRange, contextPadding, FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE);
     154             :   }
     155             : 
     156             :   private CommentContext getContextForMergeList(
     157             :       ObjectReader reader, RevCommit commit, Range commentRange, int contextPadding)
     158             :       throws IOException {
     159           1 :     ComparisonType cmp = ComparisonType.againstParent(1);
     160           1 :     Text text = Text.forMergeList(cmp, reader, commit);
     161           1 :     return createContext(
     162             :         text, commentRange, contextPadding, FileContentUtil.TEXT_X_GERRIT_MERGE_LIST);
     163             :   }
     164             : 
     165             :   private CommentContext getContextForFilePath(
     166             :       Repository repo,
     167             :       RevWalk rw,
     168             :       RevCommit commit,
     169             :       String filePath,
     170             :       Range commentRange,
     171             :       int contextPadding)
     172             :       throws IOException {
     173             :     // TODO(ghareeb): We can further group the comments by file paths to avoid opening
     174             :     // the same file multiple times.
     175           1 :     try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), filePath, commit.getTree())) {
     176           1 :       if (tw == null) {
     177           0 :         logger.atWarning().log(
     178           0 :             "Could not find path %s in the git tree of ID %s.", filePath, commit.getTree().getId());
     179           0 :         return CommentContext.empty();
     180             :       }
     181           1 :       ObjectId id = tw.getObjectId(0);
     182           1 :       byte[] sourceContent = SrcContentResolver.getSourceContent(repo, id, tw.getFileMode(0));
     183           1 :       Text textSrc = new Text(sourceContent);
     184           1 :       String contentType = getContentType(tw, filePath, textSrc);
     185           1 :       return createContext(textSrc, commentRange, contextPadding, contentType);
     186           0 :     }
     187             :   }
     188             : 
     189             :   private String getContentType(TreeWalk tw, String filePath, Text src) {
     190           1 :     PatchScript.FileMode fileMode = PatchScript.FileMode.fromJgitFileMode(tw.getFileMode(0));
     191           1 :     String mimeType = MimeUtil2.UNKNOWN_MIME_TYPE.toString();
     192           1 :     if (src.size() > 0 && PatchScript.FileMode.SYMLINK != fileMode) {
     193           1 :       MimeType registryMimeType = registry.getMimeType(filePath, src.getContent());
     194           1 :       mimeType = registryMimeType.toString();
     195             :     }
     196           1 :     return FileContentUtil.resolveContentType(projectState, filePath, fileMode, mimeType);
     197             :   }
     198             : 
     199             :   private static CommentContext createContext(
     200             :       Text src, Range commentRange, int contextPadding, String contentType) {
     201           1 :     if (commentRange.start() < 1 || commentRange.end() - 1 > src.size()) {
     202             :       // TODO(ghareeb): We should throw an exception in this case. See
     203             :       // https://bugs.chromium.org/p/gerrit/issues/detail?id=14102 which is an example where the
     204             :       // diff contains an extra line not in the original file.
     205           1 :       return CommentContext.empty();
     206             :     }
     207           1 :     commentRange = adjustRange(commentRange, contextPadding, src.size());
     208           1 :     ImmutableMap.Builder<Integer, String> context =
     209           1 :         ImmutableMap.builderWithExpectedSize(commentRange.end() - commentRange.start());
     210           1 :     for (int i = commentRange.start(); i < commentRange.end(); i++) {
     211           1 :       context.put(i, src.getString(i - 1));
     212             :     }
     213           1 :     return CommentContext.create(context.build(), contentType);
     214             :   }
     215             : 
     216             :   /**
     217             :    * Adjust the {@code commentRange} parameter by adding {@code contextPadding} lines before and
     218             :    * after the comment range.
     219             :    */
     220             :   private static Range adjustRange(Range commentRange, int contextPadding, int fileLines) {
     221           1 :     int newStartLine = commentRange.start() - contextPadding;
     222           1 :     int newEndLine = commentRange.end() + contextPadding;
     223           1 :     return Range.create(Math.max(1, newStartLine), Math.min(fileLines + 1, newEndLine));
     224             :   }
     225             : 
     226             :   private static Optional<Range> getStartAndEndLines(ContextInput comment) {
     227           1 :     if (comment.range() != null) {
     228           1 :       return Optional.of(Range.create(comment.range().startLine, comment.range().endLine + 1));
     229           1 :     } else if (comment.lineNumber() > 0) {
     230           1 :       return Optional.of(Range.create(comment.lineNumber(), comment.lineNumber() + 1));
     231             :     }
     232           1 :     return Optional.empty();
     233             :   }
     234             : 
     235             :   @AutoValue
     236           1 :   abstract static class Range {
     237             :     static Range create(int start, int end) {
     238           1 :       return new AutoValue_CommentContextLoader_Range(start, end);
     239             :     }
     240             : 
     241             :     /** Start line of the comment (inclusive). */
     242             :     abstract int start();
     243             : 
     244             :     /** End line of the comment (exclusive). */
     245             :     abstract int end();
     246             : 
     247             :     /** Number of lines covered by this range. */
     248             :     int size() {
     249           0 :       return end() - start();
     250             :     }
     251             :   }
     252             : 
     253             :   /** This entity only contains comment fields needed to load the comment context. */
     254             :   @AutoValue
     255           1 :   abstract static class ContextInput {
     256             :     static ContextInput fromComment(Comment comment, int contextPadding) {
     257           1 :       return new AutoValue_CommentContextLoader_ContextInput.Builder()
     258           1 :           .commitId(comment.getCommitId())
     259           1 :           .filePath(comment.key.filename)
     260           1 :           .range(comment.range)
     261           1 :           .lineNumber(comment.lineNbr)
     262           1 :           .contextPadding(contextPadding)
     263           1 :           .build();
     264             :     }
     265             : 
     266             :     /** 20 bytes SHA-1 of the patchset commit containing the file where the comment is written. */
     267             :     abstract ObjectId commitId();
     268             : 
     269             :     /** File path where the comment is written. */
     270             :     abstract String filePath();
     271             : 
     272             :     /**
     273             :      * Position of the comment in the file (start line, start char, end line, end char). This field
     274             :      * can be null if the range is not available for this comment.
     275             :      */
     276             :     @Nullable
     277             :     abstract Comment.Range range();
     278             : 
     279             :     /**
     280             :      * The 1-based line number where the comment is written. A value 0 means that the line number is
     281             :      * not available for this comment.
     282             :      */
     283             :     abstract Integer lineNumber();
     284             : 
     285             :     /** Number of extra lines of context that should be added before and after the comment range. */
     286             :     abstract Integer contextPadding();
     287             : 
     288             :     @AutoValue.Builder
     289           1 :     public abstract static class Builder {
     290             : 
     291             :       public abstract Builder commitId(ObjectId commitId);
     292             : 
     293             :       public abstract Builder filePath(String filePath);
     294             : 
     295             :       public abstract Builder range(@Nullable Comment.Range range);
     296             : 
     297             :       public abstract Builder lineNumber(Integer lineNumber);
     298             : 
     299             :       public abstract Builder contextPadding(Integer contextPadding);
     300             : 
     301             :       public abstract ContextInput build();
     302             :     }
     303             :   }
     304             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750