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

com.google.gerrit.server.patch.SubmitWithStickyApprovalDiff Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc6
Show newest version
// Copyright (C) 2021 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;

import static com.google.gerrit.server.project.ProjectCache.illegalState;

import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.diff.DiffInfoCreator;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.LargeObjectException;
import com.google.gerrit.server.git.validators.CommentCumulativeSizeValidator;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.TemporaryBuffer;

/**
 * This class is used on submit to compute the diff between the latest approved patch-set, and the
 * current submitted patch-set.
 *
 * 

Latest approved patch-set is defined by the latest patch-set which has Code-Review label voted * with the maximum possible value. * *

If the latest approved patch-set is the same as the submitted patch-set, the diff will be * empty. * *

We exclude the magic files from the returned diff to make it shorter and more concise. */ public class SubmitWithStickyApprovalDiff { private static final int HEAP_EST_SIZE = 32 * 1024; private static final int DEFAULT_POST_SUBMIT_SIZE_LIMIT = 300 * 1024; // 300 KiB private final DiffOperations diffOperations; private final ProjectCache projectCache; private final PatchScriptFactory.Factory patchScriptFactoryFactory; private final GitRepositoryManager repositoryManager; private final int maxCumulativeSize; @Inject SubmitWithStickyApprovalDiff( DiffOperations diffOperations, ProjectCache projectCache, PatchScriptFactory.Factory patchScriptFactoryFactory, GitRepositoryManager repositoryManager, @GerritServerConfig Config serverConfig) { this.diffOperations = diffOperations; this.projectCache = projectCache; this.patchScriptFactoryFactory = patchScriptFactoryFactory; this.repositoryManager = repositoryManager; // (November 2021) We define the max cumulative comment size to 300 KIB since it's a reasonable // size that is large enough for all purposes but not too large to choke the change index by // exceeding the cumulative comment size limit (new comments are not allowed once the limit // is reached). At Google, the change index limit is 5MB, while the cumulative size limit is // set at 3MB. In this example, we can reach at most 3.3MB hence we ensure not to exceed the // limit of 5MB. // The reason we exclude the post submit diff from the cumulative comment size limit is // just because change messages not currently being validated. Change messages are still // counted towards the limit, though. maxCumulativeSize = serverConfig.getInt( "change", "cumulativeCommentSizeLimit", CommentCumulativeSizeValidator.DEFAULT_CUMULATIVE_COMMENT_SIZE_LIMIT); } public String apply(ChangeNotes notes, CurrentUser currentUser) throws AuthException, IOException, PermissionBackendException, InvalidChangeOperationException { PatchSet currentPatchset = notes.getCurrentPatchSet(); Optional latestApprovedPatchsetId = getLatestApprovedPatchsetId(notes); if (latestApprovedPatchsetId.isEmpty() || latestApprovedPatchsetId.get().get() == currentPatchset.id().get()) { // If the latest approved patchset is the current patchset, no need to return anything. return ""; } StringBuilder diff = new StringBuilder( String.format( "\n\n%d is the latest approved patch-set.\n", latestApprovedPatchsetId.get().get())); Map modifiedFiles = listModifiedFiles( notes.getProjectName(), currentPatchset, notes.getPatchSets().get(latestApprovedPatchsetId.get())); // To make the message a bit more concise, we skip the magic files. List modifiedFilesList = modifiedFiles.values().stream() .filter(p -> !Patch.isMagic(p.newPath().orElse(""))) .collect(Collectors.toList()); if (modifiedFilesList.isEmpty()) { diff.append( "No files were changed between the latest approved patch-set and the submitted one.\n"); return diff.toString(); } diff.append("The change was submitted with unreviewed changes in the following files:\n\n"); TemporaryBuffer.Heap buffer = new TemporaryBuffer.Heap( Math.min(HEAP_EST_SIZE, DEFAULT_POST_SUBMIT_SIZE_LIMIT), DEFAULT_POST_SUBMIT_SIZE_LIMIT); try (Repository repository = repositoryManager.openRepository(notes.getProjectName()); DiffFormatter formatter = new DiffFormatter(buffer)) { formatter.setRepository(repository); formatter.setDetectRenames(true); boolean isDiffTooLarge = false; List formatterResult = null; try { formatter.format( modifiedFilesList.get(0).oldCommitId(), modifiedFilesList.get(0).newCommitId()); // This returns the diff for all the files. formatterResult = Arrays.stream(RawParseUtils.decode(buffer.toByteArray()).split("\n")) .collect(Collectors.toList()); } catch (IOException e) { if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) { isDiffTooLarge = true; } else { throw e; } } if (formatterResult != null) { int addedBytes = formatterResult.stream().mapToInt(String::length).sum(); if (!CommentCumulativeSizeValidator.isEnoughSpace(notes, addedBytes, maxCumulativeSize)) { isDiffTooLarge = true; } } for (FileDiffOutput fileDiff : modifiedFilesList) { diff.append( getDiffForFile( notes, currentPatchset.id(), latestApprovedPatchsetId.get(), fileDiff, currentUser, formatterResult, isDiffTooLarge)); } } return diff.toString(); } private String getDiffForFile( ChangeNotes notes, PatchSet.Id currentPatchsetId, PatchSet.Id latestApprovedPatchsetId, FileDiffOutput fileDiffOutput, CurrentUser currentUser, @Nullable List formatterResult, boolean isDiffTooLarge) throws AuthException, InvalidChangeOperationException, IOException, PermissionBackendException { StringBuilder diff = new StringBuilder( String.format( "```\nThe name of the file: %s\nInsertions: %d, Deletions: %d.\n\n", fileDiffOutput.newPath().isPresent() ? fileDiffOutput.newPath().get() : fileDiffOutput.oldPath().get(), fileDiffOutput.insertions(), fileDiffOutput.deletions())); DiffPreferencesInfo diffPreferencesInfo = createDefaultDiffPreferencesInfo(); PatchScriptFactory patchScriptFactory = patchScriptFactoryFactory.create( notes, fileDiffOutput.newPath().isPresent() ? fileDiffOutput.newPath().get() : fileDiffOutput.oldPath().get(), latestApprovedPatchsetId, currentPatchsetId, diffPreferencesInfo, currentUser); PatchScript patchScript = null; try { // TODO(paiking): we can get rid of this call to optimize by checking the diff for renames. patchScript = patchScriptFactory.call(); } catch (LargeObjectException exception) { diff.append("The file content is too large for showing the full diff. \n\n"); return diff.toString(); } if (patchScript.getChangeType() == ChangeType.RENAMED) { diff.append( String.format( "The file %s was renamed to %s\n", fileDiffOutput.oldPath().get(), fileDiffOutput.newPath().get())); } if (isDiffTooLarge) { diff.append("The diff is too large to show. Please review the diff."); diff.append("\n```\n"); return diff.toString(); } // This filters only the file we need. // TODO(paiking): we can make this more efficient by mapping the files to their respective // diffs prior to this method, such that we need to go over the diff only once. diff.append(getDiffForFile(patchScript, formatterResult)); // This line (and the ``` above) are useful for formatting in the web UI. diff.append("\n```\n"); return diff.toString(); } /** * Show patch set as unified difference for a specific file. We on purpose are not using {@link * DiffInfoCreator} since we'd like to get the original git/JGit style diff. */ public String getDiffForFile(PatchScript patchScript, List formatterResult) { // only return information about the current file, and not about files that are not // relevant. DiffFormatter returns other potential files because of rebases, which we can // ignore. List modifiedFormatterResult = new ArrayList<>(); int indexOfFormatterResult = 0; while (formatterResult.size() > indexOfFormatterResult && !formatterResult .get(indexOfFormatterResult) .equals( String.format( "diff --git a/%s b/%s", patchScript.getOldName() != null ? patchScript.getOldName() : patchScript.getNewName(), patchScript.getNewName()))) { indexOfFormatterResult++; } // remove non user friendly information. while (formatterResult.size() > indexOfFormatterResult && !formatterResult.get(indexOfFormatterResult).startsWith("@@")) { indexOfFormatterResult++; } for (; indexOfFormatterResult < formatterResult.size(); indexOfFormatterResult++) { if (formatterResult.get(indexOfFormatterResult).startsWith("diff --git")) { break; } modifiedFormatterResult.add(formatterResult.get(indexOfFormatterResult)); } if (modifiedFormatterResult.size() == 0) { // This happens for diffs that are just renames, but we already account for renames. return ""; } return modifiedFormatterResult.stream() .filter(s -> !s.equals("\\ No newline at end of file")) .collect(Collectors.joining("\n")); } private DiffPreferencesInfo createDefaultDiffPreferencesInfo() { DiffPreferencesInfo diffPreferencesInfo = new DiffPreferencesInfo(); diffPreferencesInfo.ignoreWhitespace = Whitespace.IGNORE_NONE; diffPreferencesInfo.intralineDifference = true; return diffPreferencesInfo; } private Optional getLatestApprovedPatchsetId(ChangeNotes notes) { ProjectState projectState = projectCache.get(notes.getProjectName()).orElseThrow(illegalState(notes.getProjectName())); Optional maxPatchSetId = Optional.empty(); for (PatchSetApproval patchSetApproval : notes.getApprovals().onlyNonCopied().values()) { if (!patchSetApproval.label().equals(LabelId.CODE_REVIEW)) { continue; } Optional lt = projectState.getLabelTypes(notes).byLabel(patchSetApproval.labelId()); if (!lt.isPresent() || !lt.get().isMaxPositive(patchSetApproval)) { continue; } if (maxPatchSetId.isEmpty() || patchSetApproval.patchSetId().get() > maxPatchSetId.get().get()) { maxPatchSetId = Optional.of(patchSetApproval.patchSetId()); } } return maxPatchSetId; } /** * Gets the list of modified files between the two latest patch-sets. Can be used to compute * difference in files between those two patch-sets. */ private Map listModifiedFiles( Project.NameKey project, PatchSet ps, PatchSet priorPatchSet) { try { return diffOperations.listModifiedFiles( project, priorPatchSet.commitId(), ps.commitId(), DiffOptions.DEFAULTS); } catch (DiffNotAvailableException ex) { throw new StorageException( "failed to compute difference in files, so won't post diff messsage on submit although " + "the latest approved patch-set was not the same as the submitted patch-set.", ex); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy