| // 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.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID; |
| import static com.google.gerrit.server.project.ProjectCache.illegalState; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.gerrit.entities.BooleanProjectConfig; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Project.NameKey; |
| import com.google.gerrit.extensions.api.changes.ApplyPatchPatchSetInput; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; |
| import com.google.gerrit.extensions.restapi.Response; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.extensions.restapi.RestModifyView; |
| import com.google.gerrit.server.ChangeUtil; |
| import com.google.gerrit.server.change.ChangeResource; |
| import com.google.gerrit.server.git.CodeReviewCommit; |
| import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; |
| import com.google.gerrit.server.git.CommitUtil; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.project.ContributorAgreementsChecker; |
| import com.google.gerrit.server.project.InvalidChangeOperationException; |
| import com.google.gerrit.server.project.NoSuchProjectException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.InternalChangeQuery; |
| import com.google.gerrit.server.update.UpdateException; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.util.List; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.patch.PatchApplier; |
| import org.eclipse.jgit.patch.PatchApplier.Result.Error; |
| import org.eclipse.jgit.revwalk.FooterLine; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| |
| @Singleton |
| public class ApplyPatch implements RestModifyView<ChangeResource, ApplyPatchPatchSetInput> { |
| |
| private final ContributorAgreementsChecker contributorAgreements; |
| private final GitRepositoryManager gitManager; |
| private final Provider<InternalChangeQuery> queryProvider; |
| private final ProjectCache projectCache; |
| private final ChangeUtil changeUtil; |
| private final PatchSetCreator patchSetCreator; |
| |
| @Inject |
| ApplyPatch( |
| ContributorAgreementsChecker contributorAgreements, |
| GitRepositoryManager gitManager, |
| Provider<InternalChangeQuery> queryProvider, |
| ProjectCache projectCache, |
| ChangeUtil changeUtil, |
| PatchSetCreator patchSetCreator) { |
| this.contributorAgreements = contributorAgreements; |
| this.gitManager = gitManager; |
| this.queryProvider = queryProvider; |
| this.projectCache = projectCache; |
| this.changeUtil = changeUtil; |
| this.patchSetCreator = patchSetCreator; |
| } |
| |
| @Override |
| public Response<ChangeInfo> apply(ChangeResource rsrc, ApplyPatchPatchSetInput input) |
| throws IOException, UpdateException, RestApiException, PermissionBackendException, |
| ConfigInvalidException, NoSuchProjectException, InvalidChangeOperationException { |
| if (input == null || input.patch == null || input.patch.patch == null) { |
| throw new BadRequestException("patch required"); |
| } |
| |
| NameKey project = rsrc.getProject(); |
| contributorAgreements.check(project, rsrc.getUser()); |
| BranchNameKey destBranch = rsrc.getChange().getDest(); |
| |
| try (Repository repo = gitManager.openRepository(project); |
| // This inserter and revwalk *must* be passed to any BatchUpdates |
| // created later on, to ensure the applied commit is flushed |
| // before patch sets are updated. |
| ObjectInserter oi = repo.newObjectInserter(); |
| ObjectReader reader = oi.newReader(); |
| CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) { |
| Ref destRef = repo.getRefDatabase().exactRef(destBranch.branch()); |
| if (destRef == null) { |
| throw new ResourceNotFoundException( |
| String.format("Branch %s does not exist.", destBranch.branch())); |
| } |
| ChangeData destChange = rsrc.getChangeData(); |
| patchSetCreator.validateChangeCanBeAppended(destChange, destBranch); |
| |
| if (!Strings.isNullOrEmpty(input.base) && Boolean.TRUE.equals(input.amend)) { |
| throw new BadRequestException("amend only works with existing revisions. omit base."); |
| } |
| |
| RevCommit latestPatchset = revWalk.parseCommit(destChange.currentPatchSet().commitId()); |
| RevCommit baseCommit; |
| ImmutableList<RevCommit> parents; |
| if (!Strings.isNullOrEmpty(input.base)) { |
| baseCommit = |
| CommitUtil.getBaseCommit( |
| project.get(), queryProvider.get(), revWalk, destRef, input.base); |
| parents = ImmutableList.of(baseCommit); |
| } else { |
| if (latestPatchset.getParentCount() != 1) { |
| throw new BadRequestException( |
| String.format( |
| "Cannot parse base commit for a change with none or multiple parents. Change ID:" |
| + " %s.", |
| destChange.getId())); |
| } |
| if (Boolean.TRUE.equals(input.amend)) { |
| baseCommit = latestPatchset; |
| parents = ImmutableList.copyOf(baseCommit.getParents()); |
| } else { |
| baseCommit = revWalk.parseCommit(latestPatchset.getParent(0)); |
| parents = ImmutableList.of(baseCommit); |
| } |
| } |
| |
| List<ListChangesOption> opts = input.responseFormatOptions; |
| if (opts == null) { |
| opts = ImmutableList.of(); |
| } |
| |
| PatchApplier.Result applyResult = |
| ApplyPatchUtil.applyPatch(repo, oi, input.patch, baseCommit); |
| |
| String commitMessage = |
| buildFullCommitMessage( |
| project, |
| latestPatchset, |
| input, |
| ApplyPatchUtil.getResultPatch( |
| repo, reader, baseCommit, revWalk.lookupTree(applyResult.getTreeId())), |
| applyResult.getErrors()); |
| |
| ChangeInfo changeInfo = |
| patchSetCreator.createPatchSetWithSuppliedTree( |
| project, |
| destChange, |
| latestPatchset, |
| parents, |
| input.author, |
| opts, |
| repo, |
| oi, |
| revWalk, |
| applyResult.getTreeId(), |
| commitMessage); |
| if (changeInfo.containsGitConflicts == null |
| && applyResult.getErrors().stream().anyMatch(Error::isGitConflict)) { |
| changeInfo.containsGitConflicts = true; |
| } |
| return Response.ok(changeInfo); |
| } |
| } |
| |
| private String buildFullCommitMessage( |
| NameKey project, |
| RevCommit latestPatchset, |
| ApplyPatchPatchSetInput input, |
| String resultPatch, |
| List<org.eclipse.jgit.patch.PatchApplier.Result.Error> errors) |
| throws ResourceConflictException, BadRequestException { |
| boolean hasInputCommitMessage = !Strings.isNullOrEmpty(input.commitMessage); |
| String fullMessage = |
| hasInputCommitMessage ? input.commitMessage : latestPatchset.getFullMessage(); |
| // Since we might add error information to the message, we need to split the footers from the |
| // actual description. |
| // TODO: Fix parsing footers from the commit message. FooterLine#fromMessage expects the raw |
| // commit message that contains header lines, see RawParseUtils#commitMessage which is invoked |
| // from FooterLine#fromMessage. RawParseUtils#commitMessage always increases the pointer by 46 |
| // to skip the "tree ..." line and if this line is not present the parsing of the footers is |
| // broken. This can lead to no footers being found although a Change-Id footer is present. This |
| // causes us to add the Change-Id again and as a result we end up with a commit message that |
| // contains the Change-Id line twice. |
| List<FooterLine> footerLines = FooterLine.fromMessage(fullMessage); |
| String messageWithNoFooters = removeFooters(fullMessage, footerLines); |
| if (FooterLine.getValues(footerLines, FOOTER_CHANGE_ID).isEmpty()) { |
| footerLines.add( |
| latestPatchset.getFooterLines().stream() |
| .filter(f -> f.matches(FOOTER_CHANGE_ID)) |
| .findFirst() |
| .get()); |
| } |
| String commitMessage = |
| ApplyPatchUtil.buildCommitMessage( |
| messageWithNoFooters, footerLines, input.patch, resultPatch, errors); |
| |
| boolean changeIdRequired = |
| projectCache |
| .get(project) |
| .orElseThrow(illegalState(project)) |
| .is(BooleanProjectConfig.REQUIRE_CHANGE_ID); |
| changeUtil.ensureChangeIdIsCorrect( |
| changeIdRequired, changeUtil.getChangeIdsFromFooter(latestPatchset).get(0), commitMessage); |
| |
| return commitMessage; |
| } |
| |
| private String removeFooters(String originalMessage, List<FooterLine> footerLines) { |
| if (footerLines.isEmpty()) { |
| return originalMessage; |
| } |
| return originalMessage.substring(0, originalMessage.indexOf(footerLines.get(0).getKey())); |
| } |
| } |