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.edit.tree; 16 : 17 : import static com.google.common.collect.ImmutableList.toImmutableList; 18 : import static java.util.Objects.requireNonNull; 19 : 20 : import com.google.common.collect.ImmutableList; 21 : import java.io.IOException; 22 : import java.util.ArrayList; 23 : import java.util.List; 24 : import org.eclipse.jgit.dircache.DirCache; 25 : import org.eclipse.jgit.dircache.DirCacheBuilder; 26 : import org.eclipse.jgit.dircache.DirCacheEditor; 27 : import org.eclipse.jgit.dircache.DirCacheEntry; 28 : import org.eclipse.jgit.lib.ObjectId; 29 : import org.eclipse.jgit.lib.ObjectInserter; 30 : import org.eclipse.jgit.lib.ObjectReader; 31 : import org.eclipse.jgit.lib.Repository; 32 : import org.eclipse.jgit.revwalk.RevCommit; 33 : 34 : /** 35 : * A creator for a new Git tree. To create the new tree, the tree of another commit is taken as a 36 : * basis and modified. Alternatively, an empty tree can serve as base. 37 : */ 38 : public class TreeCreator { 39 : 40 : private final ObjectId baseTreeId; 41 : private final ImmutableList<? extends ObjectId> baseParents; 42 32 : private final List<TreeModification> treeModifications = new ArrayList<>(); 43 : 44 : public static TreeCreator basedOn(RevCommit baseCommit) { 45 21 : requireNonNull(baseCommit, "baseCommit is required"); 46 21 : return new TreeCreator(baseCommit.getTree(), ImmutableList.copyOf(baseCommit.getParents())); 47 : } 48 : 49 : public static TreeCreator basedOnTree( 50 : ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) { 51 14 : requireNonNull(baseTreeId, "baseTreeId is required"); 52 14 : return new TreeCreator(baseTreeId, baseParents); 53 : } 54 : 55 : public static TreeCreator basedOnEmptyTree() { 56 3 : return new TreeCreator(ObjectId.zeroId(), ImmutableList.of()); 57 : } 58 : 59 32 : private TreeCreator(ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) { 60 32 : this.baseTreeId = requireNonNull(baseTreeId, "baseTree is required"); 61 32 : this.baseParents = baseParents; 62 32 : } 63 : 64 : /** 65 : * Apply modifications to the tree which is taken as a basis. If this method is called multiple 66 : * times, the modifications are applied subsequently in exactly the order they were provided 67 : * (though JGit applies some internal optimizations which involve sorting, too). 68 : * 69 : * <p><strong>Beware:</strong> All provided {@link TreeModification}s (even from previous calls of 70 : * this method) must touch different file paths! 71 : * 72 : * @param treeModifications modifications which should be applied to the base tree 73 : */ 74 : public void addTreeModifications(List<TreeModification> treeModifications) { 75 32 : requireNonNull(treeModifications, "treeModifications must not be null"); 76 32 : this.treeModifications.addAll(treeModifications); 77 32 : } 78 : 79 : /** 80 : * Creates the new tree. When this method is called, the specified base tree is read from the 81 : * repository, the specified modifications are applied, and the resulting tree is written to the 82 : * object store of the repository. 83 : * 84 : * @param repository the affected Git repository 85 : * @return the {@code ObjectId} of the created tree 86 : * @throws IOException if problems arise when accessing the repository 87 : */ 88 : public ObjectId createNewTreeAndGetId(Repository repository) throws IOException { 89 32 : ensureTreeModificationsDoNotTouchSameFiles(); 90 32 : DirCache newTree = createNewTree(repository); 91 32 : return writeAndGetId(repository, newTree); 92 : } 93 : 94 : private void ensureTreeModificationsDoNotTouchSameFiles() { 95 : // The current implementation of TreeCreator doesn't properly support modifications which touch 96 : // the same files even if they are provided in a logical order. One reason for this is that 97 : // JGit's DirCache implementation sorts the given path edits which is necessary due to the 98 : // nature of the Git index. The internal sorting doesn't seem to be the only issue, though. Even 99 : // applying the modifications in batches within different, subsequent DirCaches just held in 100 : // memory didn't seem to work. We might need to fully write each batch to disk before creating 101 : // the next. 102 32 : ImmutableList<String> filePaths = 103 32 : treeModifications.stream() 104 32 : .flatMap(treeModification -> treeModification.getFilePaths().stream()) 105 32 : .collect(toImmutableList()); 106 32 : long distinctFilePathNum = filePaths.stream().distinct().count(); 107 32 : if (filePaths.size() != distinctFilePathNum) { 108 1 : throw new IllegalStateException( 109 1 : String.format( 110 : "TreeModifications must not refer to the same file paths. This would have" 111 : + " unexpected/wrong behavior! Found file paths: %s.", 112 : filePaths)); 113 : } 114 32 : } 115 : 116 : private DirCache createNewTree(Repository repository) throws IOException { 117 32 : DirCache newTree = readBaseTree(repository); 118 32 : List<DirCacheEditor.PathEdit> pathEdits = getPathEdits(repository); 119 32 : applyPathEdits(newTree, pathEdits); 120 32 : return newTree; 121 : } 122 : 123 : private DirCache readBaseTree(Repository repository) throws IOException { 124 32 : try (ObjectReader objectReader = repository.newObjectReader()) { 125 32 : DirCache dirCache = DirCache.newInCore(); 126 32 : DirCacheBuilder dirCacheBuilder = dirCache.builder(); 127 32 : if (!ObjectId.zeroId().equals(baseTreeId)) { 128 31 : dirCacheBuilder.addTree(new byte[0], DirCacheEntry.STAGE_0, objectReader, baseTreeId); 129 : } 130 32 : dirCacheBuilder.finish(); 131 32 : return dirCache; 132 : } 133 : } 134 : 135 : private List<DirCacheEditor.PathEdit> getPathEdits(Repository repository) throws IOException { 136 32 : List<DirCacheEditor.PathEdit> pathEdits = new ArrayList<>(); 137 32 : for (TreeModification treeModification : treeModifications) { 138 27 : pathEdits.addAll( 139 27 : treeModification.getPathEdits(repository, baseTreeId, ImmutableList.copyOf(baseParents))); 140 27 : } 141 32 : return pathEdits; 142 : } 143 : 144 : private static void applyPathEdits(DirCache tree, List<DirCacheEditor.PathEdit> pathEdits) { 145 32 : DirCacheEditor dirCacheEditor = tree.editor(); 146 32 : pathEdits.forEach(dirCacheEditor::add); 147 32 : dirCacheEditor.finish(); 148 32 : } 149 : 150 : private static ObjectId writeAndGetId(Repository repository, DirCache tree) throws IOException { 151 32 : try (ObjectInserter objectInserter = repository.newObjectInserter()) { 152 32 : ObjectId treeId = tree.writeTree(objectInserter); 153 32 : objectInserter.flush(); 154 32 : return treeId; 155 : } 156 : } 157 : }