blob: 7265604fee94965a015cfedb1e132e9f131443ca [file] [log] [blame]
// Copyright (C) 2014 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.googlesource.gerrit.plugins.batch.util;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.submit.IntegrationException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.concurrent.Callable;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.Merger;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.StringUtils;
public class MergeBuilder implements Callable<ObjectId> {
/**
* The modes available for fast forward merges corresponding to the --ff, --no-ff and --ff-only
* options
*/
public static enum FastForwardMode {
/**
* Corresponds to the default --ff option (for a fast forward update the branch pointer only).
*/
FF("ff"),
/** Corresponds to the --no-ff option (create a merge commit even for a fast forward). */
NO_FF("no-ff"),
/**
* Corresponds to the --ff-only option (abort unless the merge is a fast forward or branch is
* already up-to-date).
*/
FF_ONLY("ff-only");
public final String name;
private FastForwardMode(final String name) {
this.name = name;
}
public String getName() {
return name;
}
public static FastForwardMode fromString(String str) {
if (StringUtils.isEmptyOrNull(str)) {
return null;
}
for (FastForwardMode mode : FastForwardMode.values()) {
if (mode.getName().equalsIgnoreCase(str)) {
return mode;
}
}
return null;
}
}
public interface Factory {
MergeBuilder create(
@Assisted Project.NameKey project,
@Assisted("message") @Nullable String message,
@Assisted @Nullable MergeStrategy strategy,
@Assisted @Nullable FastForwardMode fastForwardMode,
@Assisted("firstParent") ObjectId firstParent,
@Assisted("secondParent") ObjectId secondParent);
}
protected final GitRepositoryManager repoManager;
protected final PersonIdent gerrit;
protected final IdentifiedUser user;
protected final Project.NameKey project;
protected String message;
protected final MergeStrategy strategy;
protected FastForwardMode fastForwardMode = FastForwardMode.FF;
protected final ObjectId firstParent;
protected final ObjectId secondParent;
@Inject
MergeBuilder(
GitRepositoryManager repoManager,
@GerritPersonIdent PersonIdent gerrit,
IdentifiedUser user,
@Assisted Project.NameKey project,
@Assisted("message") @Nullable String message,
@Assisted @Nullable MergeStrategy strategy,
@Assisted @Nullable FastForwardMode fastForwardMode,
@Assisted("firstParent") ObjectId firstParent,
@Assisted("secondParent") ObjectId secondParent) {
this.repoManager = repoManager;
this.gerrit = gerrit;
this.user = user;
this.project = project;
this.message = message;
this.strategy = strategy;
if (fastForwardMode != null) {
this.fastForwardMode = fastForwardMode;
}
this.firstParent = firstParent;
this.secondParent = secondParent;
}
@Override
public ObjectId call() throws IOException, IntegrationException {
try (Repository repo = repoManager.openRepository(project)) {
return build(repo);
}
}
public ObjectId build(Repository repo) throws IOException, IntegrationException {
try (RevWalk revWalk = new RevWalk(repo)) {
RevCommit firstParentCommit = revWalk.lookupCommit(firstParent);
RevCommit secondParentCommit = revWalk.lookupCommit(secondParent);
if (revWalk.isMergedInto(secondParentCommit, firstParentCommit)) {
return firstParent; // already up to date
}
if (fastForwardMode != FastForwardMode.NO_FF
&& revWalk.isMergedInto(firstParentCommit, secondParentCommit)) {
return secondParent; // Fast forward merge
}
if (fastForwardMode == FastForwardMode.FF_ONLY) {
throw new IntegrationException("Merge aborted"); // because not FF
}
return merge(repo, revWalk);
}
}
protected ObjectId merge(Repository repo, RevWalk revWalk)
throws IOException, IntegrationException {
ThreeWayMerger merger = getMerger(repo);
if (!merger.merge(firstParent, secondParent)) {
throw new IntegrationException("Merge conflict");
}
message = defaultMessage(revWalk, message);
return insert(merger, buildCommit(merger));
}
protected ThreeWayMerger getMerger(Repository repo) {
if (strategy == MergeStrategy.RESOLVE) {
return MergeStrategy.RESOLVE.newMerger(repo, true);
}
return MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
}
protected CommitBuilder buildCommit(Merger merger) {
final CommitBuilder mergeCommit = new CommitBuilder();
mergeCommit.setTreeId(merger.getResultTreeId());
mergeCommit.setParentIds(firstParent, secondParent);
mergeCommit.setAuthor(
user.newCommitterIdent(new Timestamp(System.currentTimeMillis()), gerrit.getTimeZone()));
mergeCommit.setCommitter(gerrit);
mergeCommit.setMessage(message);
return mergeCommit;
}
protected String defaultMessage(RevWalk walk, String message) {
if (message == null) {
try {
message = walk.parseCommit(secondParent).getShortMessage();
} catch (Exception e) {
message = secondParent.getName();
}
message = "Merge \"" + message + "\"";
}
return message;
}
protected ObjectId insert(Merger merger, CommitBuilder commit) throws IOException {
try (ObjectInserter objInserter = merger.getObjectInserter()) {
ObjectId mergeCommitId = objInserter.insert(commit);
objInserter.flush();
return mergeCommitId;
}
}
}