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 : }