| // Copyright (C) 2023 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.change; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.MoreCollectors; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.ParentCommitData; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.InternalChangeQuery; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.Optional; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.ReachabilityChecker; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| @Singleton |
| public class ParentDataProvider { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final Provider<InternalChangeQuery> queryProvider; |
| |
| @Inject |
| public ParentDataProvider(Provider<InternalChangeQuery> queryProvider) { |
| this.queryProvider = queryProvider; |
| } |
| |
| /** |
| * Returns data about a specific {@code revCommit}, specifically whether it's merged in a {@code |
| * targetBranch}, or if it's a patch-set commit of some Gerrit change otherwise. This can be used |
| * to get more information of parent commits of patch-sets. |
| */ |
| public ParentCommitData get( |
| Project.NameKey project, Repository repo, ObjectId parentCommitId, String targetBranch) { |
| boolean inTargetBranch = isMergedInTargetBranch(project, repo, parentCommitId, targetBranch); |
| Optional<ParentCommitData> fromGerritChange = |
| getFromGerritChange(project, parentCommitId, targetBranch); |
| if (fromGerritChange.isEmpty()) { |
| return ParentCommitData.builder() |
| .branchName(Optional.of(targetBranch)) |
| .commitId(Optional.of(parentCommitId)) |
| .isMergedInTargetBranch(inTargetBranch) |
| .autoBuild(); |
| } |
| return fromGerritChange |
| .map(f -> f.toBuilder().isMergedInTargetBranch(inTargetBranch).autoBuild()) |
| .get(); |
| } |
| |
| /** Returns true if the parent commit {@code parentCommitId} is merged in the target branch. */ |
| private boolean isMergedInTargetBranch( |
| Project.NameKey project, Repository repo, ObjectId parentCommitId, String targetBranch) { |
| try (RevWalk rw = new RevWalk(repo); |
| ObjectReader reader = repo.newObjectReader()) { |
| Ref targetBranchRef = repo.exactRef(targetBranch); |
| if (targetBranchRef == null) { |
| return false; |
| } |
| RevCommit parent = rw.parseCommit(parentCommitId); |
| RevCommit targetBranchCommit = rw.parseCommit(targetBranchRef.getObjectId()); |
| ReachabilityChecker checker = reader.createReachabilityChecker(rw); |
| Optional<RevCommit> unreachable = |
| checker.areAllReachable( |
| ImmutableList.of(parent), ImmutableList.of(targetBranchCommit).stream()); |
| return unreachable.isEmpty(); |
| } catch (IOException e) { |
| logger.atWarning().withCause(e).log( |
| "Failed to check if parent commit %s (project: %s) is merged into target branch %s", |
| parentCommitId.name(), project, targetBranch); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns {@link ParentCommitData} if there is a change associated with {@code parentCommitId}. |
| */ |
| private Optional<ParentCommitData> getFromGerritChange( |
| Project.NameKey project, ObjectId parentCommitId, String targetBranch) { |
| List<ChangeData> changeData = queryProvider.get().byCommit(parentCommitId.name()); |
| if (changeData.size() > 1) { |
| logger.atWarning().log( |
| "Found more than one change associated with parent revision %s (project: %s). Found" |
| + " changes %s.", |
| parentCommitId.name(), |
| project.get(), |
| changeData.stream().map(ChangeData::getId).collect(ImmutableList.toImmutableList())); |
| } |
| if (changeData.size() != 1) { |
| return Optional.empty(); |
| } |
| ChangeData singleData = changeData.get(0); |
| int patchSetNumber = |
| singleData.patchSets().stream() |
| .filter(p -> p.commitId().equals(parentCommitId)) |
| .collect(MoreCollectors.onlyElement()) |
| .number(); |
| return Optional.of( |
| ParentCommitData.builder() |
| .branchName(Optional.of(targetBranch)) |
| .commitId(Optional.of(parentCommitId)) |
| .changeKey(Optional.of(singleData.change().getKey())) |
| .changeNumber(Optional.of(singleData.getId().get())) |
| .patchSetNumber(Optional.of(patchSetNumber)) |
| .changeStatus(Optional.of(singleData.change().getStatus())) |
| .autoBuild()); |
| } |
| } |