| // Copyright (C) 2013 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.project.ProjectCache.illegalState; | |
| import com.google.gerrit.entities.BranchOrderSection; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.entities.SubmitTypeRecord; | |
| import com.google.gerrit.extensions.client.SubmitType; | |
| import com.google.gerrit.extensions.common.MergeableInfo; | |
| import com.google.gerrit.extensions.restapi.AuthException; | |
| import com.google.gerrit.extensions.restapi.BadRequestException; | |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; | |
| import com.google.gerrit.extensions.restapi.Response; | |
| import com.google.gerrit.extensions.restapi.RestReadView; | |
| import com.google.gerrit.server.ChangeUtil; | |
| import com.google.gerrit.server.change.MergeabilityCache; | |
| import com.google.gerrit.server.change.MergeabilityComputationBehavior; | |
| import com.google.gerrit.server.change.RevisionResource; | |
| import com.google.gerrit.server.config.GerritServerConfig; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.git.MergeUtilFactory; | |
| import com.google.gerrit.server.index.change.ChangeIndexer; | |
| import com.google.gerrit.server.project.ProjectCache; | |
| import com.google.gerrit.server.project.ProjectState; | |
| import com.google.gerrit.server.project.SubmitRuleEvaluator; | |
| import com.google.gerrit.server.project.SubmitRuleOptions; | |
| import com.google.gerrit.server.query.change.ChangeData; | |
| import com.google.inject.Inject; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import java.util.concurrent.Future; | |
| import org.eclipse.jgit.lib.Config; | |
| import org.eclipse.jgit.lib.Constants; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.Ref; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.kohsuke.args4j.Option; | |
| public class Mergeable implements RestReadView<RevisionResource> { | |
| @Option( | |
| name = "--other-branches", | |
| aliases = {"-o"}, | |
| usage = "test mergeability for other branches too") | |
| private boolean otherBranches; | |
| private final Config cfg; | |
| private final GitRepositoryManager gitManager; | |
| private final ProjectCache projectCache; | |
| private final MergeUtilFactory mergeUtilFactory; | |
| private final ChangeData.Factory changeDataFactory; | |
| private final ChangeIndexer indexer; | |
| private final MergeabilityCache cache; | |
| private final SubmitRuleEvaluator submitRuleEvaluator; | |
| @Inject | |
| Mergeable( | |
| @GerritServerConfig Config cfg, | |
| GitRepositoryManager gitManager, | |
| ProjectCache projectCache, | |
| MergeUtilFactory mergeUtilFactory, | |
| ChangeData.Factory changeDataFactory, | |
| ChangeIndexer indexer, | |
| MergeabilityCache cache, | |
| SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) { | |
| this.cfg = cfg; | |
| this.gitManager = gitManager; | |
| this.projectCache = projectCache; | |
| this.mergeUtilFactory = mergeUtilFactory; | |
| this.changeDataFactory = changeDataFactory; | |
| this.indexer = indexer; | |
| this.cache = cache; | |
| submitRuleEvaluator = submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults()); | |
| } | |
| public void setOtherBranches(boolean otherBranches) { | |
| this.otherBranches = otherBranches; | |
| } | |
| @Override | |
| public Response<MergeableInfo> apply(RevisionResource resource) | |
| throws AuthException, ResourceConflictException, BadRequestException, IOException { | |
| Change change = resource.getChange(); | |
| PatchSet ps = resource.getPatchSet(); | |
| MergeableInfo result = new MergeableInfo(); | |
| if (!change.isNew()) { | |
| throw new ResourceConflictException("change is " + ChangeUtil.status(change)); | |
| } else if (!ps.id().equals(change.currentPatchSetId())) { | |
| // Only the current revision is mergeable. Others always fail. | |
| return Response.ok(result); | |
| } | |
| ChangeData cd = changeDataFactory.create(resource.getNotes()); | |
| result.submitType = getSubmitType(cd); | |
| try (Repository git = gitManager.openRepository(change.getProject())) { | |
| ObjectId commit = ps.commitId(); | |
| Ref ref = git.getRefDatabase().exactRef(change.getDest().branch()); | |
| ProjectState projectState = | |
| projectCache.get(change.getProject()).orElseThrow(illegalState(change.getProject())); | |
| String strategy = mergeUtilFactory.create(projectState).mergeStrategyName(); | |
| result.strategy = strategy; | |
| result.mergeable = isMergable(git, change, commit, ref, result.submitType, strategy); | |
| if (otherBranches) { | |
| result.mergeableInto = new ArrayList<>(); | |
| Optional<BranchOrderSection> branchOrder = projectState.getBranchOrderSection(); | |
| if (branchOrder.isPresent()) { | |
| int prefixLen = Constants.R_HEADS.length(); | |
| List<String> names = branchOrder.get().getMoreStable(ref.getName()); | |
| Map<String, Ref> refs = | |
| git.getRefDatabase().exactRef(names.toArray(new String[names.size()])); | |
| for (String n : names) { | |
| Ref other = refs.get(n); | |
| if (other == null) { | |
| continue; | |
| } | |
| if (cache.get(commit, other, SubmitType.CHERRY_PICK, strategy, change.getDest(), git)) { | |
| result.mergeableInto.add(other.getName().substring(prefixLen)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return Response.ok(result); | |
| } | |
| private SubmitType getSubmitType(ChangeData cd) throws ResourceConflictException { | |
| SubmitTypeRecord rec = submitRuleEvaluator.getSubmitType(cd); | |
| if (rec.status != SubmitTypeRecord.Status.OK) { | |
| throw new ResourceConflictException("submit type rule error: " + rec.errorMessage); | |
| } | |
| return rec.type; | |
| } | |
| private boolean isMergable( | |
| Repository git, | |
| Change change, | |
| ObjectId commit, | |
| Ref ref, | |
| SubmitType submitType, | |
| String strategy) { | |
| if (commit == null) { | |
| return false; | |
| } | |
| Boolean old = cache.getIfPresent(commit, ref, submitType, strategy); | |
| if (old != null) { | |
| return old; | |
| } | |
| return refresh(change, commit, ref, submitType, strategy, git, old); | |
| } | |
| private boolean refresh( | |
| final Change change, | |
| ObjectId commit, | |
| final Ref ref, | |
| SubmitType type, | |
| String strategy, | |
| Repository git, | |
| Boolean old) { | |
| boolean mergeable = cache.get(commit, ref, type, strategy, change.getDest(), git); | |
| // TODO(dborowitz): Include something else in the change ETag that it's possible to bump here, | |
| // such as cache or secondary index update time. | |
| if (!Objects.equals(mergeable, old) | |
| && MergeabilityComputationBehavior.fromConfig(cfg).includeInIndex()) { | |
| @SuppressWarnings("unused") | |
| Future<?> possiblyIgnoredError = indexer.indexAsync(change.getProject(), change.getId()); | |
| } | |
| return mergeable; | |
| } | |
| } |