All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.edit.tree.TreeCreator Maven / Gradle / Ivy

The newest version!
// 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 baseParents;
  private final Optional objectInserter;
  private final Optional objectReader;
  private final List 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 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 baseParents,
      Optional objectInserter,
      Optional 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).
   *
   * 

Beware: 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 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 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 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 getPathEdits(Repository repository) throws IOException { List 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 pathEdits) { DirCacheEditor dirCacheEditor = tree.editor(); pathEdits.forEach(dirCacheEditor::add); dirCacheEditor.finish(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy