| // 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(); |
| } |
| } |