summaryrefslogtreecommitdiffstats
path: root/mgrapp/src/com/google/codereview/manager/merge
diff options
context:
space:
mode:
Diffstat (limited to 'mgrapp/src/com/google/codereview/manager/merge')
-rw-r--r--mgrapp/src/com/google/codereview/manager/merge/BranchUpdater.java69
-rw-r--r--mgrapp/src/com/google/codereview/manager/merge/CodeReviewCommit.java51
-rw-r--r--mgrapp/src/com/google/codereview/manager/merge/MergeException.java26
-rw-r--r--mgrapp/src/com/google/codereview/manager/merge/MergeOp.java386
-rw-r--r--mgrapp/src/com/google/codereview/manager/merge/MergeSorter.java96
-rw-r--r--mgrapp/src/com/google/codereview/manager/merge/PendingMerger.java197
-rw-r--r--mgrapp/src/com/google/codereview/manager/merge/UpdateOp.java130
7 files changed, 955 insertions, 0 deletions
diff --git a/mgrapp/src/com/google/codereview/manager/merge/BranchUpdater.java b/mgrapp/src/com/google/codereview/manager/merge/BranchUpdater.java
new file mode 100644
index 0000000000..3304d747bf
--- /dev/null
+++ b/mgrapp/src/com/google/codereview/manager/merge/BranchUpdater.java
@@ -0,0 +1,69 @@
+// 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.PostBranchUpdate.PostBranchUpdateRequest;
+import com.google.codereview.internal.PostBranchUpdate.PostBranchUpdateResponse;
+import com.google.codereview.internal.PostBuildResult.PostBuildResultResponse;
+import com.google.codereview.manager.Backend;
+import com.google.codereview.rpc.SimpleController;
+import com.google.protobuf.RpcCallback;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class BranchUpdater {
+ private static final Log LOG = LogFactory.getLog(BranchUpdater.class);
+
+ private final Backend server;
+
+ public BranchUpdater(final Backend be) {
+ server = be;
+ }
+
+ public void updateBranch(final PostBuildResultResponse buildInfo) {
+ if (new UpdateOp(server.getRepositoryCache(), buildInfo).update()) {
+ final PostBranchUpdateRequest.Builder req;
+
+ req = PostBranchUpdateRequest.newBuilder();
+ req.setBranchKey(buildInfo.getDestBranchKey());
+ req.addAllNewChange(buildInfo.getNewChangeList());
+ send(req.build());
+ } else {
+ final PostBranchUpdateRequest.Builder req;
+
+ req = PostBranchUpdateRequest.newBuilder();
+ req.setBranchKey(buildInfo.getDestBranchKey());
+ // Don't mark any changes merged.
+ send(req.build());
+ }
+ }
+
+ private void send(final PostBranchUpdateRequest msg) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("\n" + msg);
+ }
+
+ final SimpleController ctrl = new SimpleController();
+ server.getMergeService().postBranchUpdate(ctrl, msg,
+ new RpcCallback<PostBranchUpdateResponse>() {
+ public void run(final PostBranchUpdateResponse rsp) {
+ }
+ });
+ if (ctrl.failed()) {
+ LOG.warn("postBranchUpdate failed: " + ctrl.errorText());
+ }
+ }
+}
diff --git a/mgrapp/src/com/google/codereview/manager/merge/CodeReviewCommit.java b/mgrapp/src/com/google/codereview/manager/merge/CodeReviewCommit.java
new file mode 100644
index 0000000000..7d64ebb0e9
--- /dev/null
+++ b/mgrapp/src/com/google/codereview/manager/merge/CodeReviewCommit.java
@@ -0,0 +1,51 @@
+// 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.PostMergeResult.MergeResultItem;
+
+import org.spearce.jgit.lib.AnyObjectId;
+import org.spearce.jgit.revwalk.RevCommit;
+
+/** Extended commit entity with code review specific metadata. */
+class CodeReviewCommit extends RevCommit {
+ /**
+ * Unique key of the PatchSet entity from the code review system.
+ * <p>
+ * This value is only available on commits that have a PatchSet represented in
+ * the code review system and whose PatchSet is in the current submit queue.
+ * Merge commits created during the merge or commits that aren't in the submit
+ * queue will keep this member null.
+ */
+ String patchsetKey;
+
+ /**
+ * Ordinal position of this commit within the submit queue.
+ * <p>
+ * Only valid if {@link #patchsetKey} is not null.
+ */
+ int originalOrder;
+
+ /**
+ * The result status for this commit.
+ * <p>
+ * Only valid if {@link #patchsetKey} is not null.
+ */
+ MergeResultItem.CodeType statusCode;
+
+ CodeReviewCommit(final AnyObjectId id) {
+ super(id);
+ }
+}
diff --git a/mgrapp/src/com/google/codereview/manager/merge/MergeException.java b/mgrapp/src/com/google/codereview/manager/merge/MergeException.java
new file mode 100644
index 0000000000..3928a33859
--- /dev/null
+++ b/mgrapp/src/com/google/codereview/manager/merge/MergeException.java
@@ -0,0 +1,26 @@
+// 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;
+
+/** Indicates the current branch's queue cannot be processed at this time. */
+class MergeException extends Exception {
+ MergeException(final String msg) {
+ super(msg, null);
+ }
+
+ MergeException(final String msg, final Throwable why) {
+ super(msg, why);
+ }
+}
diff --git a/mgrapp/src/com/google/codereview/manager/merge/MergeOp.java b/mgrapp/src/com/google/codereview/manager/merge/MergeOp.java
new file mode 100644
index 0000000000..aabd3e55fa
--- /dev/null
+++ b/mgrapp/src/com/google/codereview/manager/merge/MergeOp.java
@@ -0,0 +1,386 @@
+// 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();
+ }
+}
diff --git a/mgrapp/src/com/google/codereview/manager/merge/MergeSorter.java b/mgrapp/src/com/google/codereview/manager/merge/MergeSorter.java
new file mode 100644
index 0000000000..0fa45baa13
--- /dev/null
+++ b/mgrapp/src/com/google/codereview/manager/merge/MergeSorter.java
@@ -0,0 +1,96 @@
+// 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.PostMergeResult.MergeResultItem;
+
+import org.spearce.jgit.revwalk.RevCommit;
+import org.spearce.jgit.revwalk.RevCommitList;
+import org.spearce.jgit.revwalk.RevFlag;
+import org.spearce.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+class MergeSorter {
+ private final RevWalk rw;
+ private final RevCommit base;
+ private final RevFlag CAN_MERGE;
+
+ MergeSorter(final RevWalk walk, final RevCommit branchHead) {
+ rw = walk;
+ CAN_MERGE = rw.newFlag("CAN_MERGE");
+ base = branchHead;
+ }
+
+ Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> incoming)
+ throws IOException {
+ final Set<CodeReviewCommit> heads = new HashSet<CodeReviewCommit>();
+ final Set<CodeReviewCommit> sort = prepareList(incoming);
+ INCOMING: while (!sort.isEmpty()) {
+ final CodeReviewCommit n = removeOne(sort);
+
+ rw.resetRetain(CAN_MERGE);
+ rw.markStart(n);
+ if (base != null) {
+ rw.markUninteresting(base);
+ }
+
+ RevCommit c;
+ final RevCommitList<RevCommit> contents = new RevCommitList<RevCommit>();
+ while ((c = rw.next()) != null) {
+ if (!c.has(CAN_MERGE)) {
+ // We cannot merge n as it would bring something we
+ // aren't permitted to merge at this time. Drop n.
+ //
+ n.statusCode = MergeResultItem.CodeType.MISSING_DEPENDENCY;
+ continue INCOMING;
+ }
+ contents.add(c);
+ }
+
+ // Anything reachable through us is better merged by just
+ // merging us directly. So prune our ancestors out and let
+ // us merge instead.
+ //
+ sort.removeAll(contents);
+ heads.removeAll(contents);
+ heads.add(n);
+ }
+ return heads;
+ }
+
+ private Set<CodeReviewCommit> prepareList(
+ final Collection<CodeReviewCommit> in) {
+ final HashSet<CodeReviewCommit> sort = new HashSet<CodeReviewCommit>();
+ for (final CodeReviewCommit c : in) {
+ if (!c.has(CAN_MERGE)) {
+ c.add(CAN_MERGE);
+ sort.add(c);
+ }
+ }
+ return sort;
+ }
+
+ private static <T> T removeOne(final Collection<T> c) {
+ final Iterator<T> i = c.iterator();
+ final T r = i.next();
+ i.remove();
+ return r;
+ }
+}
diff --git a/mgrapp/src/com/google/codereview/manager/merge/PendingMerger.java b/mgrapp/src/com/google/codereview/manager/merge/PendingMerger.java
new file mode 100644
index 0000000000..bad3aa0bc3
--- /dev/null
+++ b/mgrapp/src/com/google/codereview/manager/merge/PendingMerger.java
@@ -0,0 +1,197 @@
+// 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.PendingMergeRequest;
+import com.google.codereview.internal.PendingMerge.PendingMergeResponse;
+import com.google.codereview.internal.PostBranchUpdate.PostBranchUpdateRequest;
+import com.google.codereview.internal.PostBranchUpdate.PostBranchUpdateResponse;
+import com.google.codereview.internal.PostBuildResult.PostBuildResultRequest;
+import com.google.codereview.internal.PostBuildResult.PostBuildResultResponse;
+import com.google.codereview.internal.PostMergeResult.PostMergeResultRequest;
+import com.google.codereview.internal.PostMergeResult.PostMergeResultResponse;
+import com.google.codereview.internal.SubmitBuild.SubmitBuildRequest;
+import com.google.codereview.internal.SubmitBuild.SubmitBuildResponse;
+import com.google.codereview.manager.Backend;
+import com.google.codereview.manager.StopProcessingException;
+import com.google.codereview.rpc.SimpleController;
+import com.google.codereview.util.MutableBoolean;
+import com.google.protobuf.RpcCallback;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.spearce.jgit.lib.ObjectId;
+
+/** Merges changes from branches with changes waiting to be merged. */
+public class PendingMerger implements Runnable {
+ private static final Log LOG = LogFactory.getLog(PendingMerger.class);
+
+ private static final PendingMergeRequest NEXT_REQ =
+ PendingMergeRequest.getDefaultInstance();
+
+ private final Backend server;
+ private final BranchUpdater updater;
+
+ public PendingMerger(final Backend be) {
+ server = be;
+ updater = new BranchUpdater(server);
+ }
+
+ public void run() {
+ try {
+ runImpl();
+ } catch (RuntimeException err) {
+ LOG.fatal("Unexpected runtime failure", err);
+ throw err;
+ } catch (Error err) {
+ LOG.fatal("Unexpected runtime failure", err);
+ throw err;
+ }
+ }
+
+ private void runImpl() {
+ boolean tryAnother;
+ do {
+ tryAnother = next();
+ } while (tryAnother);
+ }
+
+ private boolean next() {
+ final MutableBoolean tryAnother = new MutableBoolean();
+ final SimpleController ctrl = new SimpleController();
+ server.getMergeService().nextPendingMerge(ctrl, NEXT_REQ,
+ new RpcCallback<PendingMergeResponse>() {
+ public void run(final PendingMergeResponse rsp) {
+ tryAnother.value = merge(rsp);
+ }
+ });
+ if (ctrl.failed()) {
+ LOG.warn("nextPendingMerge failed: " + ctrl.errorText());
+ tryAnother.value = false;
+ }
+ return tryAnother.value;
+ }
+
+ private boolean merge(final PendingMergeResponse rsp) {
+ final PendingMergeResponse.CodeType sc = rsp.getStatusCode();
+ if (sc == PendingMergeResponse.CodeType.QUEUE_EMPTY) {
+ return false;
+ }
+
+ if (sc == PendingMergeResponse.CodeType.MERGE_READY) {
+ mergeImpl(rsp);
+ return true;
+ }
+
+ throw new StopProcessingException("unknown status " + sc.name());
+ }
+
+ protected void mergeImpl(final PendingMergeResponse rsp) {
+ final MergeOp mo = new MergeOp(server, rsp);
+ final PostMergeResultRequest result = mo.merge();
+
+ send(result);
+
+ if (mo.getMergeTip() != null && !mo.getNewChanges().isEmpty()) {
+ final SubmitBuildRequest.Builder b = SubmitBuildRequest.newBuilder();
+ b.setBranchKey(rsp.getDestBranchKey());
+ b.setRevisionId(mo.getMergeTip().name());
+ for (final CodeReviewCommit c : mo.getNewChanges()) {
+ if (c.patchsetKey != null) {
+ b.addNewChange(c.patchsetKey);
+ }
+ }
+ send(b.build(), mo.getMergeTip().getId());
+ } else {
+ final PostBranchUpdateRequest.Builder b;
+
+ b = PostBranchUpdateRequest.newBuilder();
+ b.setBranchKey(rsp.getDestBranchKey());
+ // Don't mark any changes merged.
+ send(b.build());
+ }
+ }
+
+ private void send(final PostMergeResultRequest msg) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("\n" + msg);
+ }
+
+ final SimpleController ctrl = new SimpleController();
+ server.getMergeService().postMergeResult(ctrl, msg,
+ new RpcCallback<PostMergeResultResponse>() {
+ public void run(final PostMergeResultResponse rsp) {
+ }
+ });
+ if (ctrl.failed()) {
+ LOG.warn("postMergeResult failed: " + ctrl.errorText());
+ }
+ }
+
+ private void send(final SubmitBuildRequest msg, final ObjectId id) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("\n" + msg);
+ }
+
+ final SimpleController ctrl = new SimpleController();
+ server.getBuildService().submitBuild(ctrl, msg,
+ new RpcCallback<SubmitBuildResponse>() {
+ public void run(final SubmitBuildResponse rsp) {
+ scheduleBuild(rsp, id);
+ }
+ });
+ if (ctrl.failed()) {
+ LOG.warn("submitBuild failed: " + ctrl.errorText());
+ }
+ }
+
+ private void scheduleBuild(final SubmitBuildResponse rsp, final ObjectId id) {
+ final int buildId = rsp.getBuildId();
+ LOG.debug("Merge commit " + id.name() + " is build " + buildId);
+
+ // For now assume the build was successful.
+ //
+ final PostBuildResultRequest.Builder req;
+ req = PostBuildResultRequest.newBuilder();
+ req.setBuildId(buildId);
+ req.setBuildStatus(PostBuildResultRequest.ResultType.SUCCESS);
+ final SimpleController ctrl = new SimpleController();
+ server.getBuildService().postBuildResult(ctrl, req.build(),
+ new RpcCallback<PostBuildResultResponse>() {
+ public void run(final PostBuildResultResponse rsp) {
+ updater.updateBranch(rsp);
+ }
+ });
+ if (ctrl.failed()) {
+ LOG.warn("postBuildResult failed: " + ctrl.errorText());
+ }
+ }
+
+ private void send(final PostBranchUpdateRequest msg) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("\n" + msg);
+ }
+
+ final SimpleController ctrl = new SimpleController();
+ server.getMergeService().postBranchUpdate(ctrl, msg,
+ new RpcCallback<PostBranchUpdateResponse>() {
+ public void run(final PostBranchUpdateResponse rsp) {
+ }
+ });
+ if (ctrl.failed()) {
+ LOG.warn("postBranchUpdate failed: " + ctrl.errorText());
+ }
+ }
+}
diff --git a/mgrapp/src/com/google/codereview/manager/merge/UpdateOp.java b/mgrapp/src/com/google/codereview/manager/merge/UpdateOp.java
new file mode 100644
index 0000000000..efc715deee
--- /dev/null
+++ b/mgrapp/src/com/google/codereview/manager/merge/UpdateOp.java
@@ -0,0 +1,130 @@
+// 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.PostBuildResult.PostBuildResultResponse;
+import com.google.codereview.manager.InvalidRepositoryException;
+import com.google.codereview.manager.RepositoryCache;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.spearce.jgit.lib.ObjectId;
+import org.spearce.jgit.lib.RefUpdate;
+import org.spearce.jgit.lib.Repository;
+import org.spearce.jgit.revwalk.RevCommit;
+import org.spearce.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+public class UpdateOp {
+ private static final Log LOG = LogFactory.getLog(UpdateOp.class);
+
+ private final RepositoryCache repoCache;
+ private final PostBuildResultResponse in;
+ private Repository db;
+ private RevCommit newCommit;
+ private RevWalk rw;
+ private RefUpdate branch;
+
+ UpdateOp(final RepositoryCache rc, final PostBuildResultResponse mergeInfo) {
+ repoCache = rc;
+ in = mergeInfo;
+ }
+
+ boolean update() {
+ final String loc = in.getDestProjectName() + " " + in.getDestBranchName();
+ LOG.debug("Updating " + loc);
+ try {
+ updateImpl();
+ return true;
+ } catch (MergeException ee) {
+ LOG.error("Error updating " + loc, ee);
+ return false;
+ } finally {
+ if (db != null && rw != null) {
+ unpinMerge();
+ }
+ }
+ }
+
+ private void unpinMerge() {
+ final String name = MergeOp.mergePinName(in.getRevisionId());
+ try {
+ final RefUpdate ru = db.updateRef(name);
+ ru.setNewObjectId(ru.getOldObjectId());
+ ru.delete(rw);
+ } catch (IOException err) {
+ LOG.warn("Cannot remove " + name, err);
+ }
+ }
+
+ private void updateImpl() throws MergeException {
+ openRepository();
+ openBranch();
+ parseCommit();
+ updateBranch();
+ }
+
+ private void openRepository() throws MergeException {
+ final String name = in.getDestProjectName();
+ try {
+ db = repoCache.get(name);
+ } catch (InvalidRepositoryException notGit) {
+ final String m = "Repository \"" + name + "\" unknown.";
+ throw new MergeException(m, notGit);
+ }
+ rw = new RevWalk(db);
+ }
+
+ private void openBranch() throws MergeException {
+ try {
+ branch = db.updateRef(in.getDestBranchName());
+ } catch (IOException e) {
+ throw new MergeException("Cannot open branch", e);
+ }
+ }
+
+ private void parseCommit() throws MergeException {
+ try {
+ newCommit = rw.parseCommit(ObjectId.fromString(in.getRevisionId()));
+ } catch (IllegalArgumentException e) {
+ throw new MergeException("Not a commit name: " + in.getRevisionId());
+ } catch (IOException e) {
+ throw new MergeException("Not a commit name: " + in.getRevisionId(), e);
+ }
+ }
+
+ private void updateBranch() throws MergeException {
+ branch.setForceUpdate(false);
+ branch.setNewObjectId(newCommit);
+ branch.setRefLogMessage(newCommit.getShortMessage(), false);
+
+ final RefUpdate.Result r;
+ try {
+ r = branch.update(rw);
+ } catch (IOException err) {
+ final String m = "Failure updating " + branch.getName();
+ throw new MergeException(m, err);
+ }
+
+ if (r == RefUpdate.Result.NEW) {
+ } else if (r == RefUpdate.Result.FAST_FORWARD) {
+ } else if (r == RefUpdate.Result.NO_CHANGE) {
+ } else {
+ final String m = "Failure updating " + branch.getName() + ": " + r.name();
+ throw new MergeException(m);
+ }
+ }
+}