LCOV - code coverage report
Current view: top level - server/query - FileEditsPredicate.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 64 77 83.1 %
Date: 2022-11-19 15:00:39 Functions: 7 8 87.5 %

          Line data    Source code
       1             : // Copyright (C) 2022 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.query;
      16             : 
      17             : import com.google.auto.value.AutoValue;
      18             : import com.google.common.collect.Iterables;
      19             : import com.google.common.flogger.FluentLogger;
      20             : import com.google.gerrit.common.Nullable;
      21             : import com.google.gerrit.entities.Patch;
      22             : import com.google.gerrit.server.git.GitRepositoryManager;
      23             : import com.google.gerrit.server.patch.DiffNotAvailableException;
      24             : import com.google.gerrit.server.patch.DiffOperations;
      25             : import com.google.gerrit.server.patch.DiffOptions;
      26             : import com.google.gerrit.server.patch.FilePathAdapter;
      27             : import com.google.gerrit.server.patch.Text;
      28             : import com.google.gerrit.server.patch.filediff.FileDiffOutput;
      29             : import com.google.gerrit.server.patch.filediff.TaggedEdit;
      30             : import com.google.gerrit.server.query.change.ChangeData;
      31             : import com.google.gerrit.server.query.change.SubmitRequirementPredicate;
      32             : import com.google.inject.assistedinject.Assisted;
      33             : import com.google.inject.assistedinject.AssistedInject;
      34             : import java.io.IOException;
      35             : import java.util.List;
      36             : import java.util.Map;
      37             : import java.util.regex.Pattern;
      38             : import java.util.stream.Collectors;
      39             : import org.eclipse.jgit.diff.Edit;
      40             : import org.eclipse.jgit.lib.Constants;
      41             : import org.eclipse.jgit.lib.ObjectId;
      42             : import org.eclipse.jgit.lib.ObjectReader;
      43             : import org.eclipse.jgit.lib.Repository;
      44             : import org.eclipse.jgit.revwalk.RevTree;
      45             : import org.eclipse.jgit.revwalk.RevWalk;
      46             : import org.eclipse.jgit.treewalk.TreeWalk;
      47             : 
      48             : /**
      49             :  * A submit-requirement predicate that can be used in submit requirements expressions. This
      50             :  * predicate is fulfilled if the diff between the latest patchset of the change and the base commit
      51             :  * includes a specific file path pattern with some specific content modification. The modification
      52             :  * could be an added, deleted or replaced content.
      53             :  */
      54             : public class FileEditsPredicate extends SubmitRequirementPredicate {
      55           1 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      56             : 
      57             :   private DiffOperations diffOperations;
      58             :   private GitRepositoryManager repoManager;
      59             :   private final FileEditsArgs fileEditsArgs;
      60             : 
      61             :   public interface Factory {
      62             :     FileEditsPredicate create(FileEditsArgs fileEditsArgs);
      63             :   }
      64             : 
      65             :   @AutoValue
      66           1 :   public abstract static class FileEditsArgs {
      67             :     abstract String filePattern();
      68             : 
      69             :     abstract String editPattern();
      70             : 
      71             :     public static FileEditsArgs create(String filePattern, String contentPattern) {
      72           1 :       return new AutoValue_FileEditsPredicate_FileEditsArgs(filePattern, contentPattern);
      73             :     }
      74             :   }
      75             : 
      76             :   @AssistedInject
      77             :   public FileEditsPredicate(
      78             :       DiffOperations diffOperations,
      79             :       GitRepositoryManager repoManager,
      80             :       @Assisted FileEditsPredicate.FileEditsArgs fileEditsArgs) {
      81           1 :     super("fileEdits", fileEditsArgs.filePattern() + "," + fileEditsArgs.editPattern());
      82           1 :     this.diffOperations = diffOperations;
      83           1 :     this.repoManager = repoManager;
      84           1 :     this.fileEditsArgs = fileEditsArgs;
      85           1 :   }
      86             : 
      87             :   @Override
      88             :   public boolean match(ChangeData cd) {
      89             :     try {
      90           1 :       Map<String, FileDiffOutput> modifiedFiles =
      91           1 :           diffOperations.listModifiedFilesAgainstParent(
      92           1 :               cd.project(),
      93           1 :               cd.currentPatchSet().commitId(),
      94             :               /* parentNum= */ 0,
      95             :               DiffOptions.DEFAULTS);
      96           1 :       FileDiffOutput firstDiff =
      97           1 :           Iterables.getFirst(modifiedFiles.values(), /* defaultValue= */ null);
      98           1 :       if (firstDiff == null) {
      99             :         // No available diffs. We cannot identify old and new commit IDs.
     100             :         // engine.fail();
     101           0 :         return false;
     102             :       }
     103             : 
     104           1 :       Pattern filePattern = null;
     105           1 :       Pattern editPattern = null;
     106           1 :       if (fileEditsArgs.filePattern().startsWith("^")) {
     107             :         // We validated the pattern before creating this predicate. No need to revalidate.
     108           1 :         String pattern = fileEditsArgs.filePattern();
     109           1 :         filePattern = Pattern.compile(pattern);
     110             :       }
     111           1 :       if (fileEditsArgs.editPattern().startsWith("^")) {
     112             :         // We validated the pattern before creating this predicate. No need to revalidate.
     113           1 :         String pattern = fileEditsArgs.editPattern();
     114           1 :         editPattern = Pattern.compile(pattern);
     115             :       }
     116           1 :       try (Repository repo = repoManager.openRepository(cd.project());
     117           1 :           ObjectReader reader = repo.newObjectReader();
     118           1 :           RevWalk rw = new RevWalk(reader)) {
     119             :         RevTree aTree =
     120           1 :             firstDiff.oldCommitId().equals(ObjectId.zeroId())
     121           0 :                 ? null
     122           1 :                 : rw.parseTree(firstDiff.oldCommitId());
     123           1 :         RevTree bTree = rw.parseCommit(firstDiff.newCommitId()).getTree();
     124             : 
     125           1 :         for (FileDiffOutput entry : modifiedFiles.values()) {
     126           1 :           String newName =
     127           1 :               FilePathAdapter.getNewPath(entry.oldPath(), entry.newPath(), entry.changeType());
     128           1 :           String oldName = FilePathAdapter.getOldPath(entry.oldPath(), entry.changeType());
     129             : 
     130           1 :           if (Patch.isMagic(newName)) {
     131           1 :             continue;
     132             :           }
     133             : 
     134           1 :           if (match(newName, fileEditsArgs.filePattern(), filePattern)
     135           0 :               || (oldName != null && match(oldName, fileEditsArgs.filePattern(), filePattern))) {
     136           1 :             List<Edit> edits =
     137           1 :                 entry.edits().stream().map(TaggedEdit::jgitEdit).collect(Collectors.toList());
     138           1 :             if (edits.isEmpty()) {
     139           0 :               continue;
     140             :             }
     141             :             Text tA;
     142           1 :             if (oldName != null) {
     143           0 :               tA = load(aTree, oldName, reader);
     144             :             } else {
     145           1 :               tA = load(aTree, newName, reader);
     146             :             }
     147           1 :             Text tB = load(bTree, newName, reader);
     148           1 :             for (Edit edit : edits) {
     149           1 :               if (tA != Text.EMPTY) {
     150           1 :                 String aDiff = tA.getString(edit.getBeginA(), edit.getEndA(), true);
     151           1 :                 if (match(aDiff, fileEditsArgs.editPattern(), editPattern)) {
     152           1 :                   return true;
     153             :                 }
     154             :               }
     155           1 :               if (tB != Text.EMPTY) {
     156           1 :                 String bDiff = tB.getString(edit.getBeginB(), edit.getEndB(), true);
     157           1 :                 if (match(bDiff, fileEditsArgs.editPattern(), editPattern)) {
     158           1 :                   return true;
     159             :                 }
     160             :               }
     161           1 :             }
     162             :           }
     163           1 :         }
     164           1 :       } catch (IOException e) {
     165           0 :         logger.atSevere().withCause(e).log("Error while evaluating commit edits.");
     166           0 :         return false;
     167           1 :       }
     168           0 :     } catch (DiffNotAvailableException e) {
     169           0 :       logger.atSevere().withCause(e).log("Diff error while evaluating commit edits.");
     170           0 :       return false;
     171           1 :     }
     172           1 :     return false;
     173             :   }
     174             : 
     175             :   @Override
     176             :   public int getCost() {
     177           0 :     return 10;
     178             :   }
     179             : 
     180             :   private Text load(@Nullable ObjectId tree, String path, ObjectReader reader) throws IOException {
     181           1 :     if (tree == null || path == null) {
     182           0 :       return Text.EMPTY;
     183             :     }
     184           1 :     final TreeWalk tw = TreeWalk.forPath(reader, path, tree);
     185           1 :     if (tw == null) {
     186           1 :       return Text.EMPTY;
     187             :     }
     188           1 :     if (tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) {
     189           0 :       return Text.EMPTY;
     190             :     }
     191           1 :     return new Text(reader.open(tw.getObjectId(0), Constants.OBJ_BLOB));
     192             :   }
     193             : 
     194             :   private boolean match(String text, String search, @Nullable Pattern searchPattern) {
     195           1 :     return searchPattern == null ? text.contains(search) : searchPattern.matcher(text).find();
     196             :   }
     197             : }

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