blob: aabd3e55fa6ccdb6c0ffd43a5760e0846b4bef62 [file] [log] [blame]
// Copyright 2008 Google Inc.
//
// 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.codereview.manager.merge;
import com.google.codereview.internal.PendingMerge.PendingMergeItem;
import com.google.codereview.internal.PendingMerge.PendingMergeResponse;
import com.google.codereview.internal.PostMergeResult.MergeResultItem;
import com.google.codereview.internal.PostMergeResult.PostMergeResultRequest;
import com.google.codereview.manager.Backend;
import com.google.codereview.manager.InvalidRepositoryException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.spearce.jgit.errors.IncorrectObjectTypeException;
import org.spearce.jgit.errors.MissingObjectException;
import org.spearce.jgit.lib.AnyObjectId;
import org.spearce.jgit.lib.Commit;
import org.spearce.jgit.lib.Constants;
import org.spearce.jgit.lib.ObjectId;
import org.spearce.jgit.lib.PersonIdent;
import org.spearce.jgit.lib.Ref;
import org.spearce.jgit.lib.RefUpdate;
import org.spearce.jgit.lib.Repository;
import org.spearce.jgit.merge.MergeStrategy;
import org.spearce.jgit.merge.Merger;
import org.spearce.jgit.revwalk.RevCommit;
import org.spearce.jgit.revwalk.RevSort;
import org.spearce.jgit.revwalk.RevWalk;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Merges changes in submission order into a single branch.
* <p>
* Branches are reduced to the minimum number of heads needed to merge
* everything. This allows commits to be entered into the queue in any order
* (such as ancestors before descendants) and only the most recent commit on any
* line of development will be merged. All unmerged commits along a line of
* development must be in the submission queue in order to merge the tip of that
* line.
* <p>
* Conflicts are handled by discarding the entire line of development and
* marking it as conflicting, even if an earlier commit along that same line can
* be merged cleanly.
*/
class MergeOp {
private static final Log LOG = LogFactory.getLog(MergeOp.class);
static String mergePinName(final AnyObjectId id) {
return mergePinName(id.name());
}
static String mergePinName(final String idstr) {
return "refs/merges/" + idstr;
}
private final Backend server;
private final PendingMergeResponse in;
private final PersonIdent mergeIdent;
private final Collection<MergeResultItem> updates;
private final List<CodeReviewCommit> toMerge;
private Repository db;
private RevWalk rw;
private CodeReviewCommit branchTip;
private CodeReviewCommit mergeTip;
private final List<CodeReviewCommit> newChanges;
MergeOp(final Backend be, final PendingMergeResponse mergeInfo) {
server = be;
in = mergeInfo;
mergeIdent = server.newMergeIdentity();
updates = new ArrayList<MergeResultItem>();
toMerge = new ArrayList<CodeReviewCommit>();
newChanges = new ArrayList<CodeReviewCommit>();
}
PostMergeResultRequest merge() {
final String loc = in.getDestProjectName() + " " + in.getDestBranchName();
LOG.debug("Merging " + loc);
try {
mergeImpl();
final PostMergeResultRequest.Builder update;
update = PostMergeResultRequest.newBuilder();
update.setDestBranchKey(in.getDestBranchKey());
update.addAllChange(updates);
return update.build();
} catch (MergeException ee) {
LOG.error("Error merging " + loc, ee);
mergeTip = null;
final PostMergeResultRequest.Builder update;
update = PostMergeResultRequest.newBuilder();
update.setDestBranchKey(in.getDestBranchKey());
for (final PendingMergeItem pmi : in.getChangeList()) {
update.addChange(suspend(pmi));
}
return update.build();
}
}
CodeReviewCommit getMergeTip() {
return mergeTip;
}
Collection<CodeReviewCommit> getNewChanges() {
return Collections.unmodifiableCollection(newChanges);
}
private void mergeImpl() throws MergeException {
openRepository();
openBranch();
validateChangeList();
reduceToMinimalMerge();
mergeTopics();
markCleanMerges();
pinMergeCommit();
}
private void openRepository() throws MergeException {
final String name = in.getDestProjectName();
try {
db = server.getRepositoryCache().get(name);
} catch (InvalidRepositoryException notGit) {
final String m = "Repository \"" + name + "\" unknown.";
throw new MergeException(m, notGit);
}
rw = new RevWalk(db) {
@Override
protected RevCommit createCommit(final AnyObjectId id) {
return new CodeReviewCommit(id);
}
};
}
private void openBranch() throws MergeException {
try {
final RefUpdate ru = db.updateRef(in.getDestBranchName());
if (ru.getOldObjectId() != null) {
branchTip = (CodeReviewCommit) rw.parseCommit(ru.getOldObjectId());
} else {
branchTip = null;
}
} catch (IOException e) {
throw new MergeException("Cannot open branch", e);
}
}
private void validateChangeList() throws MergeException {
final Set<ObjectId> tips = new HashSet<ObjectId>();
for (final Ref r : db.getAllRefs().values()) {
tips.add(r.getObjectId());
}
int commitOrder = 0;
for (final PendingMergeItem pmi : in.getChangeList()) {
final String idstr = pmi.getRevisionId();
final ObjectId id;
try {
id = ObjectId.fromString(idstr);
} catch (IllegalArgumentException iae) {
throw new MergeException("Invalid ObjectId: " + idstr);
}
if (!tips.contains(id)) {
// TODO Technically the proper way to do this test is to use a
// RevWalk on "$id --not --all" and test for an empty set. But
// that is way slower than looking for a ref directly pointing
// at the desired tip. We should always have a ref available.
//
// TODO this is actually an error, the branch is gone but we
// want to merge the issue. We can't safely do that if the
// tip is not reachable.
LOG.error("Cannot find branch head for " + id.name());
updates.add(suspend(pmi));
continue;
}
final CodeReviewCommit commit;
try {
commit = (CodeReviewCommit) rw.parseCommit(id);
} catch (IOException e) {
throw new MergeException("Invalid issue commit " + id, e);
}
commit.patchsetKey = pmi.getPatchsetKey();
commit.originalOrder = commitOrder++;
LOG.debug("Commit " + commit.name() + " is " + commit.patchsetKey);
if (branchTip != null) {
// If this commit is already merged its a bug in the queuing code
// that we got back here. Just mark it complete and move on. Its
// merged and that is all that mattered to the requestor.
//
try {
if (rw.isMergedInto(commit, branchTip)) {
commit.statusCode = MergeResultItem.CodeType.ALREADY_MERGED;
updates.add(toResult(commit));
LOG.debug("Already merged " + commit.name());
continue;
}
} catch (IOException err) {
throw new MergeException("Cannot perform merge base test", err);
}
}
toMerge.add(commit);
}
}
private void reduceToMinimalMerge() throws MergeException {
final Collection<CodeReviewCommit> heads;
try {
heads = new MergeSorter(rw, branchTip).sort(toMerge);
} catch (IOException e) {
throw new MergeException("Branch head sorting failed", e);
}
for (final CodeReviewCommit c : toMerge) {
if (c.statusCode != null) {
updates.add(toResult(c));
}
}
toMerge.clear();
toMerge.addAll(heads);
Collections.sort(toMerge, new Comparator<CodeReviewCommit>() {
public int compare(final CodeReviewCommit a, final CodeReviewCommit b) {
return a.originalOrder - b.originalOrder;
}
});
}
private void mergeTopics() throws MergeException {
mergeTip = branchTip;
// Take the first fast-forward available, if any is available in the set.
//
for (final Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext();) {
try {
final CodeReviewCommit n = i.next();
if (mergeTip == null || rw.isMergedInto(mergeTip, n)) {
mergeTip = n;
i.remove();
LOG.debug("Fast-forward to " + n.name());
break;
}
} catch (IOException e) {
throw new MergeException("Cannot fast-forward test during merge", e);
}
}
// For every other commit do a pair-wise merge.
//
while (!toMerge.isEmpty()) {
final CodeReviewCommit n = toMerge.remove(0);
final Merger m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db);
try {
if (m.merge(new AnyObjectId[] {mergeTip, n})) {
writeMergeCommit(m, n);
LOG.debug("Merged " + n.name());
} else {
rw.reset();
rw.markStart(n);
rw.markUninteresting(mergeTip);
CodeReviewCommit failed;
while ((failed = (CodeReviewCommit) rw.next()) != null) {
if (failed.patchsetKey != null) {
failed.statusCode = MergeResultItem.CodeType.PATH_CONFLICT;
updates.add(toResult(failed));
}
}
LOG.debug("Rejected (path conflict) " + n.name());
}
} catch (IOException e) {
throw new MergeException("Cannot merge " + n.name(), e);
}
}
}
private void writeMergeCommit(final Merger m, final CodeReviewCommit n)
throws IOException, MissingObjectException, IncorrectObjectTypeException {
final Commit mergeCommit = new Commit(db);
mergeCommit.setTreeId(m.getResultTreeId());
mergeCommit.setParentIds(new ObjectId[] {mergeTip, n});
mergeCommit.setAuthor(mergeIdent);
mergeCommit.setCommitter(mergeCommit.getAuthor());
mergeCommit.setMessage("Merge");
final ObjectId id = m.getObjectWriter().writeCommit(mergeCommit);
mergeTip = (CodeReviewCommit) rw.parseCommit(id);
}
private void markCleanMerges() throws MergeException {
try {
rw.reset();
rw.sort(RevSort.REVERSE);
rw.markStart(mergeTip);
if (branchTip != null) {
rw.markUninteresting(branchTip);
} else {
for (final Ref r : db.getAllRefs().values()) {
if (r.getName().startsWith(Constants.R_HEADS)
|| r.getName().startsWith(Constants.R_TAGS)) {
try {
rw.markUninteresting(rw.parseCommit(r.getObjectId()));
} catch (IncorrectObjectTypeException iote) {
// Not a commit? Skip over it.
}
}
}
}
CodeReviewCommit c;
while ((c = (CodeReviewCommit) rw.next()) != null) {
if (c.patchsetKey != null) {
c.statusCode = MergeResultItem.CodeType.CLEAN_MERGE;
updates.add(toResult(c));
newChanges.add(c);
}
}
} catch (IOException e) {
throw new MergeException("Cannot mark clean merges", e);
}
}
private void pinMergeCommit() throws MergeException {
final String name = mergePinName(mergeTip.getId());
final RefUpdate.Result r;
try {
final RefUpdate u = db.updateRef(name);
u.setNewObjectId(mergeTip.getId());
u.setRefLogMessage("Merged submit queue", false);
r = u.update();
} catch (IOException err) {
final String m = "Failure creating " + name;
throw new MergeException(m, err);
}
if (r == RefUpdate.Result.NEW) {
} else if (r == RefUpdate.Result.FAST_FORWARD) {
} else if (r == RefUpdate.Result.FORCED) {
} else if (r == RefUpdate.Result.NO_CHANGE) {
} else {
final String m = "Failure creating " + name + ": " + r.name();
throw new MergeException(m);
}
}
private static MergeResultItem suspend(final PendingMergeItem pmi) {
final MergeResultItem.Builder delay = MergeResultItem.newBuilder();
delay.setStatusCode(MergeResultItem.CodeType.MISSING_DEPENDENCY);
delay.setPatchsetKey(pmi.getPatchsetKey());
return delay.build();
}
private static MergeResultItem toResult(final CodeReviewCommit c) {
final MergeResultItem.Builder delay = MergeResultItem.newBuilder();
delay.setStatusCode(c.statusCode);
delay.setPatchsetKey(c.patchsetKey);
return delay.build();
}
}