diff options
Diffstat (limited to 'mgrapp/src/com/google/codereview/manager/merge')
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); + } + } +} |