blob: 47303180e5320a4607733c99015d472593a5c003 [file] [log] [blame]
// Copyright (C) 2016 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.project;
import com.google.gerrit.exceptions.InvalidMergeStrategyException;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.MergeableInfo;
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.RestReadView;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.project.BranchResource;
import com.google.inject.Inject;
import java.io.IOException;
import org.eclipse.jgit.errors.NoMergeBaseException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.Merger;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.kohsuke.args4j.Option;
/** Check the mergeability at current branch for a git object references expression. */
public class CheckMergeability implements RestReadView<BranchResource> {
private String source;
private String strategy;
private SubmitType submitType;
@Option(
name = "--source",
metaVar = "COMMIT",
usage =
"the source reference to merge, which could be any git object "
+ "references expression, refer to "
+ "org.eclipse.jgit.lib.Repository#resolve(String)",
required = true)
public void setSource(String source) {
this.source = source;
}
@Option(
name = "--strategy",
metaVar = "STRATEGY",
usage = "name of the merge strategy, refer to org.eclipse.jgit.merge.MergeStrategy")
public void setStrategy(String strategy) {
this.strategy = strategy;
}
private final GitRepositoryManager gitManager;
private final CommitsCollection commits;
@Inject
CheckMergeability(
GitRepositoryManager gitManager, CommitsCollection commits, @GerritServerConfig Config cfg) {
this.gitManager = gitManager;
this.commits = commits;
this.strategy = MergeUtil.getMergeStrategy(cfg).getName();
this.submitType = cfg.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
}
@Override
public Response<MergeableInfo> apply(BranchResource resource)
throws IOException, BadRequestException, ResourceNotFoundException,
ResourceConflictException {
if (!(submitType.equals(SubmitType.MERGE_ALWAYS)
|| submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
throw new BadRequestException("Submit type: " + submitType + " is not supported");
}
MergeableInfo result = new MergeableInfo();
result.submitType = submitType;
result.strategy = strategy;
try (Repository git = gitManager.openRepository(resource.getNameKey());
RevWalk rw = new RevWalk(git);
ObjectInserter inserter = new InMemoryInserter(git)) {
Merger m = MergeUtil.newMerger(inserter, git.getConfig(), strategy);
Ref destRef = git.getRefDatabase().exactRef(resource.getRef());
if (destRef == null) {
throw new ResourceNotFoundException(resource.getRef());
}
RevCommit targetCommit = rw.parseCommit(destRef.getObjectId());
RevCommit sourceCommit = null;
try {
sourceCommit = MergeUtil.resolveCommit(git, rw, source);
if (!commits.canRead(resource.getProjectState(), git, sourceCommit)) {
throw new BadRequestException("do not have read permission for: " + source);
}
} catch (BadRequestException e) {
// Throw a unified exception for permission denied and unresolvable commits.
throw new BadRequestException(
"Error resolving: '"
+ source
+ "'. Do not have read permission, or failed to resolve to a commit.",
e);
}
if (rw.isMergedInto(sourceCommit, targetCommit)) {
result.mergeable = true;
result.commitMerged = true;
result.contentMerged = true;
return Response.ok(result);
}
if (m.merge(false, targetCommit, sourceCommit)) {
result.mergeable = true;
result.commitMerged = false;
result.contentMerged = m.getResultTreeId().equals(targetCommit.getTree());
} else {
result.mergeable = false;
if (m instanceof ResolveMerger) {
result.conflicts = ((ResolveMerger) m).getUnmergedPaths();
}
}
} catch (InvalidMergeStrategyException e) {
throw new BadRequestException(e.getMessage());
} catch (NoMergeBaseException e) {
// TODO(ekempin) Rather return MergeableInfo with mergeable = false. But then we need a new
// field in MergeableInfo to carry the message to the client and the frontend needs to be
// adapted to show the message to the user.
throw new ResourceConflictException(
String.format("Change cannot be merged: %s", e.getMessage()), e);
}
return Response.ok(result);
}
}