| // Copyright (C) 2017 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.server.edit.tree; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import static java.util.Objects.requireNonNull; | |
| import com.google.common.collect.ImmutableList; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import org.eclipse.jgit.dircache.DirCache; | |
| import org.eclipse.jgit.dircache.DirCacheBuilder; | |
| import org.eclipse.jgit.dircache.DirCacheEditor; | |
| import org.eclipse.jgit.dircache.DirCacheEntry; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.ObjectInserter; | |
| import org.eclipse.jgit.lib.ObjectReader; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| /** | |
| * A creator for a new Git tree. To create the new tree, the tree of another commit is taken as a | |
| * basis and modified. Alternatively, an empty tree can serve as base. | |
| */ | |
| public class TreeCreator { | |
| private final ObjectId baseTreeId; | |
| private final ImmutableList<? extends ObjectId> baseParents; | |
| private final List<TreeModification> treeModifications = new ArrayList<>(); | |
| public static TreeCreator basedOn(RevCommit baseCommit) { | |
| requireNonNull(baseCommit, "baseCommit is required"); | |
| return new TreeCreator(baseCommit.getTree(), ImmutableList.copyOf(baseCommit.getParents())); | |
| } | |
| public static TreeCreator basedOnTree( | |
| ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) { | |
| requireNonNull(baseTreeId, "baseTreeId is required"); | |
| return new TreeCreator(baseTreeId, baseParents); | |
| } | |
| public static TreeCreator basedOnEmptyTree() { | |
| return new TreeCreator(ObjectId.zeroId(), ImmutableList.of()); | |
| } | |
| private TreeCreator(ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) { | |
| this.baseTreeId = requireNonNull(baseTreeId, "baseTree is required"); | |
| this.baseParents = baseParents; | |
| } | |
| /** | |
| * Apply modifications to the tree which is taken as a basis. If this method is called multiple | |
| * times, the modifications are applied subsequently in exactly the order they were provided | |
| * (though JGit applies some internal optimizations which involve sorting, too). | |
| * | |
| * <p><strong>Beware:</strong> All provided {@link TreeModification}s (even from previous calls of | |
| * this method) must touch different file paths! | |
| * | |
| * @param treeModifications modifications which should be applied to the base tree | |
| */ | |
| public void addTreeModifications(List<TreeModification> treeModifications) { | |
| requireNonNull(treeModifications, "treeModifications must not be null"); | |
| this.treeModifications.addAll(treeModifications); | |
| } | |
| /** | |
| * Creates the new tree. When this method is called, the specified base tree is read from the | |
| * repository, the specified modifications are applied, and the resulting tree is written to the | |
| * object store of the repository. | |
| * | |
| * @param repository the affected Git repository | |
| * @return the {@code ObjectId} of the created tree | |
| * @throws IOException if problems arise when accessing the repository | |
| */ | |
| public ObjectId createNewTreeAndGetId(Repository repository) throws IOException { | |
| ensureTreeModificationsDoNotTouchSameFiles(); | |
| DirCache newTree = createNewTree(repository); | |
| return writeAndGetId(repository, newTree); | |
| } | |
| private void ensureTreeModificationsDoNotTouchSameFiles() { | |
| // The current implementation of TreeCreator doesn't properly support modifications which touch | |
| // the same files even if they are provided in a logical order. One reason for this is that | |
| // JGit's DirCache implementation sorts the given path edits which is necessary due to the | |
| // nature of the Git index. The internal sorting doesn't seem to be the only issue, though. Even | |
| // applying the modifications in batches within different, subsequent DirCaches just held in | |
| // memory didn't seem to work. We might need to fully write each batch to disk before creating | |
| // the next. | |
| ImmutableList<String> filePaths = | |
| treeModifications.stream() | |
| .flatMap(treeModification -> treeModification.getFilePaths().stream()) | |
| .collect(toImmutableList()); | |
| long distinctFilePathNum = filePaths.stream().distinct().count(); | |
| if (filePaths.size() != distinctFilePathNum) { | |
| throw new IllegalStateException( | |
| String.format( | |
| "TreeModifications must not refer to the same file paths. This would have" | |
| + " unexpected/wrong behavior! Found file paths: %s.", | |
| filePaths)); | |
| } | |
| } | |
| private DirCache createNewTree(Repository repository) throws IOException { | |
| DirCache newTree = readBaseTree(repository); | |
| List<DirCacheEditor.PathEdit> pathEdits = getPathEdits(repository); | |
| applyPathEdits(newTree, pathEdits); | |
| return newTree; | |
| } | |
| private DirCache readBaseTree(Repository repository) throws IOException { | |
| try (ObjectReader objectReader = repository.newObjectReader()) { | |
| DirCache dirCache = DirCache.newInCore(); | |
| DirCacheBuilder dirCacheBuilder = dirCache.builder(); | |
| if (!ObjectId.zeroId().equals(baseTreeId)) { | |
| dirCacheBuilder.addTree(new byte[0], DirCacheEntry.STAGE_0, objectReader, baseTreeId); | |
| } | |
| dirCacheBuilder.finish(); | |
| return dirCache; | |
| } | |
| } | |
| private List<DirCacheEditor.PathEdit> getPathEdits(Repository repository) throws IOException { | |
| List<DirCacheEditor.PathEdit> pathEdits = new ArrayList<>(); | |
| for (TreeModification treeModification : treeModifications) { | |
| pathEdits.addAll( | |
| treeModification.getPathEdits(repository, baseTreeId, ImmutableList.copyOf(baseParents))); | |
| } | |
| return pathEdits; | |
| } | |
| private static void applyPathEdits(DirCache tree, List<DirCacheEditor.PathEdit> pathEdits) { | |
| DirCacheEditor dirCacheEditor = tree.editor(); | |
| pathEdits.forEach(dirCacheEditor::add); | |
| dirCacheEditor.finish(); | |
| } | |
| private static ObjectId writeAndGetId(Repository repository, DirCache tree) throws IOException { | |
| try (ObjectInserter objectInserter = repository.newObjectInserter()) { | |
| ObjectId treeId = tree.writeTree(objectInserter); | |
| objectInserter.flush(); | |
| return treeId; | |
| } | |
| } | |
| } |