| // 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 com.google.gerrit.common.UsedAt; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Optional; |
| 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 Optional<ObjectInserter> objectInserter; |
| private final Optional<ObjectReader> objectReader; |
| 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()), |
| Optional.empty(), |
| Optional.empty()); |
| } |
| |
| @UsedAt(UsedAt.Project.GOOGLE) |
| public static TreeCreator basedOn( |
| RevCommit baseCommit, ObjectInserter objectInserter, ObjectReader objectReader) { |
| requireNonNull(baseCommit, "baseCommit is required"); |
| return new TreeCreator( |
| baseCommit.getTree(), |
| ImmutableList.copyOf(baseCommit.getParents()), |
| Optional.of(objectInserter), |
| Optional.of(objectReader)); |
| } |
| |
| public static TreeCreator basedOnTree( |
| ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) { |
| requireNonNull(baseTreeId, "baseTreeId is required"); |
| return new TreeCreator(baseTreeId, baseParents, Optional.empty(), Optional.empty()); |
| } |
| |
| public static TreeCreator basedOnEmptyTree() { |
| return new TreeCreator( |
| ObjectId.zeroId(), ImmutableList.of(), Optional.empty(), Optional.empty()); |
| } |
| |
| private TreeCreator( |
| ObjectId baseTreeId, |
| ImmutableList<? extends ObjectId> baseParents, |
| Optional<ObjectInserter> objectInserter, |
| Optional<ObjectReader> objectReader) { |
| this.baseTreeId = requireNonNull(baseTreeId, "baseTree is required"); |
| this.baseParents = baseParents; |
| this.objectInserter = objectInserter; |
| this.objectReader = objectReader; |
| } |
| |
| /** |
| * 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 { |
| ObjectReader or = objectReader.orElseGet(() -> repository.newObjectReader()); |
| try { |
| DirCache dirCache = |
| ObjectId.zeroId().equals(baseTreeId) |
| ? DirCache.newInCore() |
| : DirCache.read(or, baseTreeId); |
| DirCacheBuilder dirCacheBuilder = dirCache.builder(); |
| if (!ObjectId.zeroId().equals(baseTreeId)) { |
| dirCacheBuilder.addTree(new byte[0], DirCacheEntry.STAGE_0, or, baseTreeId); |
| } |
| dirCacheBuilder.finish(); |
| return dirCache; |
| } finally { |
| if (objectReader.isEmpty()) { |
| or.close(); |
| } |
| } |
| } |
| |
| 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 ObjectId writeAndGetId(Repository repository, DirCache tree) throws IOException { |
| ObjectInserter oi = objectInserter.orElseGet(() -> repository.newObjectInserter()); |
| try { |
| ObjectId treeId = tree.writeTree(oi); |
| oi.flush(); |
| return treeId; |
| } finally { |
| if (objectInserter.isEmpty()) { |
| oi.close(); |
| } |
| } |
| } |
| |
| private static void applyPathEdits(DirCache tree, List<DirCacheEditor.PathEdit> pathEdits) { |
| DirCacheEditor dirCacheEditor = tree.editor(); |
| pathEdits.forEach(dirCacheEditor::add); |
| dirCacheEditor.finish(); |
| } |
| } |