blob: 2e0214c64741f27d6f98451722cb0a56fa57ed0e [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.patch;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.Throwables;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.RetryableAction.ActionType;
import com.google.inject.Inject;
import java.io.IOException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
/**
* Utility class for creating an auto-merge commit of a merge commit.
*
* <p>An auto-merge commit is the result of merging the 2 parents of a merge commit automatically.
* If there are conflicts the auto-merge commit contains Git conflict markers that indicate these
* conflicts.
*
* <p>Creating auto-merge commits for octopus merges (merge commits with more than 2 parents) is not
* supported. In this case the auto-merge is created between the first 2 parent commits.
*
* <p>All created auto-merge commits are stored in the repository of their merge commit as {@code
* refs/cache-automerge/} branches. These branches serve:
*
* <ul>
* <li>as a cache so that the each auto-merge gets computed only once
* <li>as base for merge commits on which users can comment
* </ul>
*
* <p>The second point means that these commits are referenced from NoteDb. The consequence of this
* is that these refs should never be deleted.
*/
public class AutoMerger {
@UsedAt(UsedAt.Project.GOOGLE)
public static boolean cacheAutomerge(Config cfg) {
return cfg.getBoolean("change", null, "cacheAutomerge", true);
}
private final RetryHelper retryHelper;
private final PersonIdent gerritIdent;
private final boolean save;
@Inject
AutoMerger(
RetryHelper retryHelper,
@GerritServerConfig Config cfg,
@GerritPersonIdent PersonIdent gerritIdent) {
this.retryHelper = retryHelper;
save = cacheAutomerge(cfg);
this.gerritIdent = gerritIdent;
}
/**
* Creates an auto-merge commit of the parents of the given merge commit.
*
* <p>In case of an exception the creation of the auto-merge commit is retried a few times. E.g.
* this allows the operation to succeed if a Git update fails due to a temporary issue.
*
* @return auto-merge commit. Headers of the returned RevCommit are parsed.
*/
public RevCommit merge(
Repository repo,
RevWalk rw,
ObjectInserter ins,
RevCommit merge,
ThreeWayMergeStrategy mergeStrategy)
throws IOException {
try {
return retryHelper
.action(
ActionType.GIT_UPDATE,
"createAutoMerge",
() -> createAutoMergeCommit(repo, rw, ins, merge, mergeStrategy))
.call();
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
Throwables.throwIfInstanceOf(e, IOException.class);
throw new IllegalStateException(e);
}
}
/**
* Creates an auto-merge commit of the parents of the given merge commit.
*
* @return auto-merge commit. Headers of the returned RevCommit are parsed.
*/
private RevCommit createAutoMergeCommit(
Repository repo,
RevWalk rw,
ObjectInserter ins,
RevCommit merge,
ThreeWayMergeStrategy mergeStrategy)
throws IOException {
checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
InMemoryInserter tmpIns = null;
if (ins instanceof InMemoryInserter) {
// Caller gave us an in-memory inserter, so ensure anything we write from
// this method is visible to them.
tmpIns = (InMemoryInserter) ins;
} else if (!save) {
// If we don't plan on saving results, use a fully in-memory inserter.
// Using just a non-flushing wrapper is not sufficient, since in
// particular DfsInserter might try to write to storage after exceeding an
// internal buffer size.
tmpIns = new InMemoryInserter(rw.getObjectReader());
}
rw.parseHeaders(merge);
String refName = RefNames.refsCacheAutomerge(merge.name());
Ref ref = repo.getRefDatabase().exactRef(refName);
if (ref != null && ref.getObjectId() != null) {
RevObject obj = rw.parseAny(ref.getObjectId());
if (obj instanceof RevCommit) {
return (RevCommit) obj;
}
return commit(repo, rw, tmpIns, ins, refName, obj, merge);
}
ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(repo, true);
DirCache dc = DirCache.newInCore();
m.setDirCache(dc);
m.setObjectInserter(tmpIns == null ? new NonFlushingWrapper(ins) : tmpIns);
boolean couldMerge = m.merge(merge.getParents());
ObjectId treeId;
if (couldMerge) {
treeId = m.getResultTreeId();
} else {
treeId =
MergeUtil.mergeWithConflicts(
rw,
ins,
dc,
"HEAD",
merge.getParent(0),
"BRANCH",
merge.getParent(1),
m.getMergeResults());
}
return commit(repo, rw, tmpIns, ins, refName, treeId, merge);
}
private RevCommit commit(
Repository repo,
RevWalk rw,
@Nullable InMemoryInserter tmpIns,
ObjectInserter ins,
String refName,
ObjectId tree,
RevCommit merge)
throws IOException {
rw.parseHeaders(merge);
// For maximum stability, choose a single ident using the committer time of
// the input commit, using the server name and timezone.
PersonIdent ident =
new PersonIdent(
gerritIdent, merge.getCommitterIdent().getWhen(), gerritIdent.getTimeZone());
CommitBuilder cb = new CommitBuilder();
cb.setAuthor(ident);
cb.setCommitter(ident);
cb.setTreeId(tree);
cb.setMessage("Auto-merge of " + merge.name() + '\n');
for (RevCommit p : merge.getParents()) {
cb.addParentId(p);
}
if (!save) {
checkArgument(tmpIns != null);
try (ObjectReader tmpReader = tmpIns.newReader();
RevWalk tmpRw = new RevWalk(tmpReader)) {
return tmpRw.parseCommit(tmpIns.insert(cb));
}
}
checkArgument(tmpIns == null);
checkArgument(!(ins instanceof InMemoryInserter));
ObjectId commitId = ins.insert(cb);
ins.flush();
RefUpdate ru = repo.updateRef(refName);
ru.setNewObjectId(commitId);
ru.disableRefLog();
switch (ru.forceUpdate()) {
case FAST_FORWARD:
case FORCED:
case NEW:
case NO_CHANGE:
return rw.parseCommit(commitId);
case LOCK_FAILURE:
throw new LockFailureException(
String.format("Failed to create auto-merge of %s", merge.name()), ru);
case IO_FAILURE:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
case RENAMED:
default:
throw new IOException(
String.format(
"Failed to create auto-merge of %s: Cannot write %s (%s)",
merge.name(), refName, ru.getResult()));
}
}
private static class NonFlushingWrapper extends ObjectInserter.Filter {
private final ObjectInserter ins;
private NonFlushingWrapper(ObjectInserter ins) {
this.ins = ins;
}
@Override
protected ObjectInserter delegate() {
return ins;
}
@Override
public void flush() {}
@Override
public void close() {}
}
}