Line data Source code
1 : // Copyright (C) 2017 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.fixes; 16 : 17 : import static com.google.common.collect.ImmutableList.toImmutableList; 18 : import static java.util.Objects.requireNonNull; 19 : import static java.util.stream.Collectors.groupingBy; 20 : 21 : import com.google.common.collect.ImmutableList; 22 : import com.google.gerrit.common.RawInputUtil; 23 : import com.google.gerrit.entities.Comment.Range; 24 : import com.google.gerrit.entities.FixReplacement; 25 : import com.google.gerrit.entities.Patch; 26 : import com.google.gerrit.extensions.restapi.BadRequestException; 27 : import com.google.gerrit.extensions.restapi.BinaryResult; 28 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 29 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 30 : import com.google.gerrit.server.change.FileContentUtil; 31 : import com.google.gerrit.server.edit.CommitModification; 32 : import com.google.gerrit.server.edit.tree.ChangeFileContentModification; 33 : import com.google.gerrit.server.edit.tree.TreeModification; 34 : import com.google.gerrit.server.patch.MagicFile; 35 : import com.google.gerrit.server.project.ProjectState; 36 : import com.google.inject.Inject; 37 : import com.google.inject.Singleton; 38 : import java.io.IOException; 39 : import java.util.List; 40 : import java.util.Map; 41 : import java.util.Objects; 42 : import org.eclipse.jgit.lib.ObjectId; 43 : import org.eclipse.jgit.lib.ObjectReader; 44 : import org.eclipse.jgit.lib.Repository; 45 : 46 : /** An interpreter for {@code FixReplacement}s. */ 47 : @Singleton 48 : public class FixReplacementInterpreter { 49 : 50 : private final FileContentUtil fileContentUtil; 51 : 52 : @Inject 53 145 : public FixReplacementInterpreter(FileContentUtil fileContentUtil) { 54 145 : this.fileContentUtil = fileContentUtil; 55 145 : } 56 : 57 : /** 58 : * Transforms the given {@code FixReplacement}s into {@code TreeModification}s. 59 : * 60 : * @param repository the affected Git repository 61 : * @param projectState the affected project 62 : * @param patchSetCommitId the patch set which should be modified 63 : * @param fixReplacements the replacements which should be applied 64 : * @return a list of {@code TreeModification}s representing the given replacements 65 : * @throws ResourceNotFoundException if a file to which one of the replacements refers doesn't 66 : * exist 67 : * @throws ResourceConflictException if the replacements can't be transformed into {@code 68 : * TreeModification}s 69 : */ 70 : public CommitModification toCommitModification( 71 : Repository repository, 72 : ProjectState projectState, 73 : ObjectId patchSetCommitId, 74 : List<FixReplacement> fixReplacements) 75 : throws BadRequestException, ResourceNotFoundException, IOException, 76 : ResourceConflictException { 77 4 : requireNonNull(fixReplacements, "Fix replacements must not be null"); 78 : 79 4 : Map<String, List<FixReplacement>> fixReplacementsPerFilePath = 80 4 : fixReplacements.stream().collect(groupingBy(fixReplacement -> fixReplacement.path)); 81 : 82 4 : CommitModification.Builder modificationBuilder = CommitModification.builder(); 83 4 : for (Map.Entry<String, List<FixReplacement>> entry : fixReplacementsPerFilePath.entrySet()) { 84 4 : if (Objects.equals(entry.getKey(), Patch.COMMIT_MSG)) { 85 2 : String newCommitMessage = 86 2 : getNewCommitMessage(repository, patchSetCommitId, entry.getValue()); 87 2 : modificationBuilder.newCommitMessage(newCommitMessage); 88 2 : } else { 89 4 : TreeModification treeModification = 90 4 : toTreeModification( 91 4 : repository, projectState, patchSetCommitId, entry.getKey(), entry.getValue()); 92 4 : modificationBuilder.addTreeModification(treeModification); 93 : } 94 4 : } 95 4 : return modificationBuilder.build(); 96 : } 97 : 98 : private static String getNewCommitMessage( 99 : Repository repository, ObjectId patchSetCommitId, List<FixReplacement> fixReplacements) 100 : throws ResourceConflictException, IOException { 101 2 : try (ObjectReader reader = repository.newObjectReader()) { 102 : // In the magic /COMMIT_MSG file, the actual commit message is placed after some generated 103 : // header lines. -> Need to find out to which actual line of the commit message a replacement 104 : // refers. 105 2 : MagicFile commitMessageFile = MagicFile.forCommitMessage(reader, patchSetCommitId); 106 2 : int commitMessageStartLine = commitMessageFile.getStartLineOfModifiableContent(); 107 : // Line numbers are 1-based. -> Add 1 to not move first line. 108 : // Move up for any additionally found lines. 109 2 : int necessaryRangeShift = -commitMessageStartLine + 1; 110 2 : ImmutableList<FixReplacement> adjustedReplacements = 111 2 : shiftRangesBy(fixReplacements, necessaryRangeShift); 112 2 : if (referToNonPositiveLine(adjustedReplacements)) { 113 2 : throw new ResourceConflictException( 114 2 : String.format("The header of the %s file cannot be modified.", Patch.COMMIT_MSG)); 115 : } 116 2 : String commitMessage = commitMessageFile.modifiableContent(); 117 2 : return FixCalculator.getNewFileContent(commitMessage, adjustedReplacements); 118 : } 119 : } 120 : 121 : private static ImmutableList<FixReplacement> shiftRangesBy( 122 : List<FixReplacement> fixReplacements, int shiftedAmount) { 123 2 : return fixReplacements.stream() 124 2 : .map(replacement -> shiftRangesBy(replacement, shiftedAmount)) 125 2 : .collect(toImmutableList()); 126 : } 127 : 128 : private static FixReplacement shiftRangesBy(FixReplacement fixReplacement, int shiftedAmount) { 129 2 : Range adjustedRange = new Range(fixReplacement.range); 130 2 : adjustedRange.startLine += shiftedAmount; 131 2 : adjustedRange.endLine += shiftedAmount; 132 2 : return new FixReplacement(fixReplacement.path, adjustedRange, fixReplacement.replacement); 133 : } 134 : 135 : private static boolean referToNonPositiveLine(List<FixReplacement> adjustedReplacements) { 136 2 : return adjustedReplacements.stream() 137 2 : .map(replacement -> replacement.range) 138 2 : .anyMatch(range -> range.startLine <= 0); 139 : } 140 : 141 : private TreeModification toTreeModification( 142 : Repository repository, 143 : ProjectState projectState, 144 : ObjectId patchSetCommitId, 145 : String filePath, 146 : List<FixReplacement> fixReplacements) 147 : throws BadRequestException, ResourceNotFoundException, IOException, 148 : ResourceConflictException { 149 4 : String fileContent = getFileContent(repository, projectState, patchSetCommitId, filePath); 150 4 : String newFileContent = FixCalculator.getNewFileContent(fileContent, fixReplacements); 151 : 152 4 : return new ChangeFileContentModification(filePath, RawInputUtil.create(newFileContent)); 153 : } 154 : 155 : private String getFileContent( 156 : Repository repository, ProjectState projectState, ObjectId patchSetCommitId, String filePath) 157 : throws ResourceNotFoundException, BadRequestException, IOException { 158 4 : try (BinaryResult fileContent = 159 4 : fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath)) { 160 4 : return fileContent.asString(); 161 : } 162 : } 163 : }