LCOV - code coverage report
Current view: top level - server/edit/tree - TreeCreator.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 50 50 100.0 %
Date: 2022-11-19 15:00:39 Functions: 13 13 100.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750