blob: 74033c1debb0f2e3924a78d9af000bf2f9b4dcfa [file] [log] [blame]
// Copyright (C) 2022 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.restapi.change;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.patch.DiffUtil;
import com.google.gerrit.server.util.CommitMessageUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.api.errors.PatchFormatException;
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.patch.PatchApplier;
import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
/** Utility for applying a patch. */
public final class ApplyPatchUtil {
/**
* Applies the given patch on top of the merge tip, using the given object inserter.
*
* @param repo to apply the patch in
* @param oi to operate with
* @param input the patch for applying
* @param mergeTip the tip to apply the patch on
* @return the tree ID with the applied patch
* @throws IOException if unable to create the jgit PatchApplier object
* @throws RestApiException for any other failure
*/
public static ObjectId applyPatch(
Repository repo, ObjectInserter oi, ApplyPatchInput input, RevCommit mergeTip)
throws IOException, RestApiException {
checkNotNull(mergeTip);
RevTree tip = mergeTip.getTree();
InputStream patchStream =
new ByteArrayInputStream(decodeIfNecessary(input.patch).getBytes(StandardCharsets.UTF_8));
try {
PatchApplier applier = new PatchApplier(repo, tip, oi);
PatchApplier.Result applyResult = applier.applyPatch(patchStream);
return applyResult.getTreeId();
} catch (PatchFormatException e) {
throw new BadRequestException("Invalid patch format: " + input.patch, e);
} catch (PatchApplyException e) {
throw RestApiException.wrap("Cannot apply patch: " + input.patch, e);
}
}
/**
* Build commit message for commits with applied patch.
*
* <p>Message structure:
*
* <ol>
* <li>Provided {@code message}.
* <li>In case the result change's patch is not the same as the original patch - a warning
* message which includes the diff; as well as the original patch's header if available, or
* the full original patch otherwise.
* <li>The provided {@code footerLines}, if any.
* </ol>
*
* @param message the first message piece, excluding footers
* @param footerLines footer lines to append to the message
* @param originalPatch to compare the result patch to
* @param resultPatch to validate accuracy for
* @return the commit message
* @throws BadRequestException if the commit message cannot be sanitized
*/
public static String buildCommitMessage(
String message, List<FooterLine> footerLines, String originalPatch, String resultPatch)
throws BadRequestException {
String decodedOriginalPatch = decodeIfNecessary(originalPatch);
StringBuilder res = new StringBuilder(message.trim());
Optional<String> patchDiff = verifyAppliedPatch(decodedOriginalPatch, resultPatch);
if (!patchDiff.isEmpty()) {
res.append(
"\n\nNOTE FOR REVIEWERS - original patch and result patch are not identical."
+ "\nPLEASE REVIEW CAREFULLY.\nDiffs between the patches:\n"
+ patchDiff.get());
}
Optional<String> originalPatchHeader = DiffUtil.getPatchHeader(decodedOriginalPatch);
if (!patchDiff.isEmpty()) {
res.append(
"\n\nOriginal patch:\n"
+ (originalPatchHeader.isEmpty() ? decodedOriginalPatch : originalPatchHeader.get()));
}
if (!footerLines.isEmpty()) {
res.append('\n');
}
for (FooterLine footer : footerLines) {
res.append("\n" + footer.toString());
}
return CommitMessageUtil.checkAndSanitizeCommitMessage(res.toString());
}
/**
* Fetch the patch of the result tree.
*
* @param repo in which the patch was applied
* @param reader for the repo objects, including {@code resultTree}
* @param baseCommit to generate patch against
* @param resultTree to generate the patch for
* @return the result patch
* @throws IOException if the result patch cannot be written
*/
public static String getResultPatch(
Repository repo, ObjectReader reader, RevCommit baseCommit, RevTree resultTree)
throws IOException {
try (OutputStream resultPatchStream = new ByteArrayOutputStream()) {
DiffUtil.getFormattedDiff(
repo, reader, baseCommit.getTree(), resultTree, null, resultPatchStream);
return resultPatchStream.toString();
}
}
private static Optional<String> verifyAppliedPatch(String originalPatch, String resultPatch) {
String cleanOriginalPatch = DiffUtil.cleanPatch(originalPatch);
String cleanResultPatch = DiffUtil.cleanPatch(resultPatch);
if (cleanOriginalPatch.equals(cleanResultPatch)) {
return Optional.empty();
}
return Optional.of(StringUtils.difference(cleanOriginalPatch, cleanResultPatch));
}
private static String decodeIfNecessary(String patch) {
if (Base64.isBase64(patch)) {
return new String(org.eclipse.jgit.util.Base64.decode(patch), StandardCharsets.UTF_8);
}
return patch;
}
private ApplyPatchUtil() {}
}