com.google.gerrit.server.patch.gitdiff.GitModifiedFilesLoader Maven / Gradle / Ivy
// Copyright (C) 2024 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.patch.gitdiff;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Patch;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.io.DisabledOutputStream;
/**
 * /** Class to load the files that have been modified between two Git trees.
 *
 * Rename detection is off unless {@link #withRenameDetection(int)} is called.
 *
 * 
The commits and the commit trees are looked up via the {@link RevWalk} instance that is
 * provided to the {@link #load(Config, ObjectReader, ObjectId, ObjectId)} method.
 */
public class GitModifiedFilesLoader {
  private static final ImmutableMap changeTypeMap =
      ImmutableMap.of(
          DiffEntry.ChangeType.ADD,
          Patch.ChangeType.ADDED,
          DiffEntry.ChangeType.MODIFY,
          Patch.ChangeType.MODIFIED,
          DiffEntry.ChangeType.DELETE,
          Patch.ChangeType.DELETED,
          DiffEntry.ChangeType.RENAME,
          Patch.ChangeType.RENAMED,
          DiffEntry.ChangeType.COPY,
          Patch.ChangeType.COPIED);
  @Nullable private Integer renameScore = null;
  /**
   * Enables rename detection
   *
   * @param renameScore the score that should be used for the rename detection.
   */
  @CanIgnoreReturnValue
  public GitModifiedFilesLoader withRenameDetection(int renameScore) {
    checkState(renameScore >= 0);
    this.renameScore = renameScore;
    return this;
  }
  /**
   * Loads the files that have been modified between {@code aTree} and {@code bTree}.
   *
   * The trees are looked up via the given {@code revWalk} instance,
   */
  public ImmutableList load(
      Config repoConfig, ObjectReader reader, ObjectId aTree, ObjectId bTree) throws IOException {
    List entries = getGitTreeDiff(repoConfig, reader, aTree, bTree);
    return entries.stream().map(GitModifiedFilesLoader::toModifiedFile).collect(toImmutableList());
  }
  private List getGitTreeDiff(
      Config repoConfig, ObjectReader reader, ObjectId aTree, ObjectId bTree) throws IOException {
    try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
      df.setReader(reader, repoConfig);
      if (renameScore != null) {
        df.setDetectRenames(true);
        df.getRenameDetector().setRenameScore(renameScore);
        // Skip detecting content renames for binary files.
        df.getRenameDetector().setSkipContentRenamesForBinaryFiles(true);
      }
      // The scan method only returns the file paths that are different. Callers may choose to
      // format these paths themselves.
      return df.scan(aTree.equals(ObjectId.zeroId()) ? null : aTree, bTree);
    }
  }
  private static ModifiedFile toModifiedFile(DiffEntry entry) {
    String oldPath = entry.getOldPath();
    String newPath = entry.getNewPath();
    return ModifiedFile.builder()
        .changeType(toChangeType(entry.getChangeType()))
        .oldPath(oldPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(oldPath))
        .newPath(newPath.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(newPath))
        .build();
  }
  private static Patch.ChangeType toChangeType(DiffEntry.ChangeType changeType) {
    if (!changeTypeMap.containsKey(changeType)) {
      throw new IllegalArgumentException("Unsupported type " + changeType);
    }
    return changeTypeMap.get(changeType);
  }
}