| // 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; |
| } |
| } |
| } |