diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java index 74d19622a5..529283b64f 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java @@ -17,6 +17,7 @@ package com.google.gerrit.common.data; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; import java.util.ArrayList; import java.util.Collection; @@ -40,6 +41,8 @@ public class ChangeDetail { protected List patchSets; protected List approvals; protected List submitRecords; + protected Project.SubmitType submitType; + protected SubmitTypeRecord submitTypeRecord; protected boolean canSubmit; protected List messages; protected PatchSet.Id currentPatchSetId; @@ -187,6 +190,14 @@ public class ChangeDetail { return submitRecords; } + public void setSubmitTypeRecord(SubmitTypeRecord submitTypeRecord) { + this.submitTypeRecord = submitTypeRecord; + } + + public SubmitTypeRecord getSubmitTypeRecord() { + return submitTypeRecord; + } + public boolean isCurrentPatchSet(final PatchSetDetail detail) { return currentPatchSetId != null && detail.getPatchSet().getId().equals(currentPatchSetId); diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java index 3c5c688f36..f3837399d0 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java @@ -19,6 +19,7 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchLineComment; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.PatchSetInfo; +import com.google.gerrit.reviewdb.client.Project; import java.util.ArrayList; import java.util.Collection; @@ -33,6 +34,7 @@ public class PatchSetPublishDetail { protected List labels; protected List approvals; protected List submitRecords; + protected SubmitTypeRecord submitTypeRecord; protected List given; protected boolean canSubmit; @@ -61,6 +63,14 @@ public class PatchSetPublishDetail { return submitRecords; } + public void setSubmitTypeRecord(SubmitTypeRecord submitTypeRecord) { + this.submitTypeRecord = submitTypeRecord; + } + + public SubmitTypeRecord getSubmitTypeRecord() { + return submitTypeRecord; + } + public List getGiven() { return given; } diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java new file mode 100644 index 0000000000..4eea7982ef --- /dev/null +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java @@ -0,0 +1,58 @@ +// Copyright (C) 2012 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.common.data; + +import com.google.gerrit.reviewdb.client.Project; + +/** + * Describes the submit type for a change. + */ +public class SubmitTypeRecord { + public static enum Status { + /** The type was computed successfully */ + OK, + + /** An internal server error occurred preventing computation. + *

+ * Additional detail may be available in {@link SubmitTypeRecord#errorMessage} + */ + RULE_ERROR + } + + public static SubmitTypeRecord OK(Project.SubmitType type) { + SubmitTypeRecord r = new SubmitTypeRecord(); + r.status = Status.OK; + r.type = type; + return r; + } + + public Status status; + public Project.SubmitType type; + public String errorMessage; + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(status); + if (status == Status.RULE_ERROR && errorMessage != null) { + sb.append('(').append(errorMessage).append(")"); + } + if (type != null) { + sb.append('['); + sb.append(type.name()); + sb.append(']'); + } + return sb.toString(); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java index d42992f63d..20d3feabc1 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java @@ -95,6 +95,7 @@ public interface ChangeConstants extends Constants { String changeInfoBlockUploaded(); String changeInfoBlockUpdated(); String changeInfoBlockStatus(); + String changeInfoBlockSubmitType(); String changePermalink(); String changeInfoBlockCanMerge(); String changeInfoBlockCanMergeYes(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties index 8ceb74caa9..56f621925d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties @@ -72,6 +72,7 @@ changeInfoBlockTopic = Topic changeInfoBlockUploaded = Uploaded changeInfoBlockUpdated = Updated changeInfoBlockStatus = Status +changeInfoBlockSubmitType = Submit Type changePermalink = Permalink changeInfoBlockCanMerge = Can Merge changeInfoBlockCanMergeYes = Yes diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java index c8b2a6658e..361f997422 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java @@ -15,8 +15,10 @@ package com.google.gerrit.client.changes; import com.google.gerrit.common.data.AccountInfoCache; +import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSetInfo; +import com.google.gerrit.reviewdb.client.Project; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwtexpui.globalkey.client.KeyCommandSet; @@ -36,8 +38,8 @@ public class ChangeDescriptionBlock extends Composite { } public void display(Change chg, Boolean starred, PatchSetInfo info, - final AccountInfoCache acc) { - infoBlock.display(chg, acc); + final AccountInfoCache acc, SubmitTypeRecord submitTypeRecord) { + infoBlock.display(chg, acc, submitTypeRecord); messageBlock.display(chg.getId(), starred, info.getMessage()); } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java index 3ffacc3e57..c42ca8d1ea 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java @@ -21,6 +21,7 @@ import com.google.gerrit.client.ui.AccountLink; import com.google.gerrit.client.ui.BranchLink; import com.google.gerrit.client.ui.ProjectLink; import com.google.gerrit.common.data.AccountInfoCache; +import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gwt.user.client.ui.Composite; @@ -36,9 +37,10 @@ public class ChangeInfoBlock extends Composite { private static final int R_TOPIC = 4; private static final int R_UPLOADED = 5; private static final int R_UPDATED = 6; - private static final int R_STATUS = 7; - private static final int R_MERGE_TEST = 8; - private static final int R_CNT = 9; + private static final int R_SUBMIT_TYPE = 7; + private static final int R_STATUS = 8; + private static final int R_MERGE_TEST = 9; + private static final int R_CNT = 10; private final Grid table; @@ -59,6 +61,7 @@ public class ChangeInfoBlock extends Composite { initRow(R_UPLOADED, Util.C.changeInfoBlockUploaded()); initRow(R_UPDATED, Util.C.changeInfoBlockUpdated()); initRow(R_STATUS, Util.C.changeInfoBlockStatus()); + initRow(R_SUBMIT_TYPE, Util.C.changeInfoBlockSubmitType()); if (Gerrit.getConfig().testChangeMerge()) { initRow(R_MERGE_TEST, Util.C.changeInfoBlockCanMerge()); } @@ -77,7 +80,8 @@ public class ChangeInfoBlock extends Composite { table.getCellFormatter().addStyleName(row, 0, Gerrit.RESOURCES.css().header()); } - public void display(final Change chg, final AccountInfoCache acc) { + public void display(final Change chg, final AccountInfoCache acc, + SubmitTypeRecord submitTypeRecord) { final Branch.NameKey dst = chg.getDest(); CopyableLabel changeIdLabel = @@ -94,6 +98,14 @@ public class ChangeInfoBlock extends Composite { table.setText(R_UPLOADED, 1, mediumFormat(chg.getCreatedOn())); table.setText(R_UPDATED, 1, mediumFormat(chg.getLastUpdatedOn())); table.setText(R_STATUS, 1, Util.toLongString(chg.getStatus())); + String submitType; + if (submitTypeRecord.status == SubmitTypeRecord.Status.OK) { + submitType = com.google.gerrit.client.admin.Util + .toLongString(submitTypeRecord.type); + } else { + submitType = submitTypeRecord.status.name(); + } + table.setText(R_SUBMIT_TYPE, 1, submitType); final Change.Status status = chg.getStatus(); if (Gerrit.getConfig().testChangeMerge()) { if (status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java index 4dd6b033a0..5868be47ab 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java @@ -282,7 +282,7 @@ public class ChangeScreen extends Screen descriptionBlock.display(detail.getChange(), detail.isStarred(), detail.getCurrentPatchSetDetail().getInfo(), - detail.getAccounts()); + detail.getAccounts(), detail.getSubmitTypeRecord()); dependsOn.display(detail.getDependsOn()); neededBy.display(detail.getNeededBy()); approvals.display(detail); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java index 4705aad452..f9d036adbc 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java @@ -274,7 +274,8 @@ public class PublishCommentScreen extends AccountScreen implements private void display(final PatchSetPublishDetail r) { setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(), patchSetId.get())); - descBlock.display(r.getChange(), null, r.getPatchSetInfo(), r.getAccounts()); + descBlock.display(r.getChange(), null, r.getPatchSetInfo(), r.getAccounts(), + r.getSubmitTypeRecord()); if (r.getChange().getStatus().isOpen()) { initApprovals(r, approvalPanel); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java index 0a934e1467..5553468ac4 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java @@ -168,6 +168,8 @@ public class ChangeDetailFactory extends Handler { } detail.setSubmitRecords(submitRecords); + detail.setSubmitTypeRecord(control.getSubmitTypeRecord(db, patch)); + patchsetsById = new HashMap(); loadPatchSets(); loadMessages(); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java index 84d964a49c..8a4320f005 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java @@ -186,6 +186,8 @@ final class PatchSetPublishDetailFactory extends Handler detail.setSubmitRecords(submitRecords); } + detail.setSubmitTypeRecord(control.getSubmitTypeRecord(db, patchSet)); + detail.setLabels(allowed); detail.setGiven(given); loadApprovals(detail, control); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java new file mode 100644 index 0000000000..7973562964 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java @@ -0,0 +1,375 @@ +// Copyright (C) 2012 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.git; + +import static com.google.gerrit.server.git.MergeUtil.commit; +import static com.google.gerrit.server.git.MergeUtil.hasMissingDependencies; +import static com.google.gerrit.server.git.MergeUtil.markCleanMerges; +import static com.google.gerrit.server.git.MergeUtil.mergeOneCommit; +import static com.google.gerrit.server.git.MergeUtil.newThreeWayMerger; + +import com.google.gerrit.common.data.ApprovalType; +import com.google.gerrit.common.data.ApprovalTypes; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.ApprovalCategory; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.PatchSetAncestor; +import com.google.gerrit.reviewdb.client.PatchSetApproval; +import com.google.gerrit.reviewdb.client.RevId; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Provider; + +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.merge.Merger; +import org.eclipse.jgit.merge.ThreeWayMerger; +import org.eclipse.jgit.revwalk.FooterKey; +import org.eclipse.jgit.revwalk.FooterLine; +import org.eclipse.jgit.revwalk.RevCommit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CherryPick extends SubmitStrategy { + private static final Logger log = LoggerFactory.getLogger(CherryPick.class); + + private static final ApprovalCategory.Id CRVW = // + new ApprovalCategory.Id("CRVW"); + private static final ApprovalCategory.Id VRIF = // + new ApprovalCategory.Id("VRIF"); + private static final FooterKey REVIEWED_ON = new FooterKey("Reviewed-on"); + private static final FooterKey CHANGE_ID = new FooterKey("Change-Id"); + + private final PatchSetInfoFactory patchSetInfoFactory; + private final Provider urlProvider; + private final ApprovalTypes approvalTypes; + private final GitReferenceUpdated replication; + private final Map newCommits; + + CherryPick(final SubmitStrategy.Arguments args, + final PatchSetInfoFactory patchSetInfoFactory, + final Provider urlProvider, final ApprovalTypes approvalTypes, + final GitReferenceUpdated replication) { + super(args); + + this.patchSetInfoFactory = patchSetInfoFactory; + this.urlProvider = urlProvider; + this.approvalTypes = approvalTypes; + this.replication = replication; + this.newCommits = new HashMap(); + } + + @Override + protected CodeReviewCommit _run(final CodeReviewCommit mergeTip, + final List toMerge) throws MergeException { + CodeReviewCommit newMergeTip = mergeTip; + while (!toMerge.isEmpty()) { + final CodeReviewCommit n = toMerge.remove(0); + final ThreeWayMerger m = + newThreeWayMerger(args.repo, args.inserter, args.useContentMerge); + try { + if (newMergeTip == null) { + // The branch is unborn. Take a fast-forward resolution to + // create the branch. + // + newMergeTip = n; + n.statusCode = CommitMergeStatus.CLEAN_MERGE; + + } else if (n.getParentCount() == 0) { + // Refuse to merge a root commit into an existing branch, + // we cannot obtain a delta for the cherry-pick to apply. + // + n.statusCode = CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT; + + } else if (n.getParentCount() == 1) { + // If there is only one parent, a cherry-pick can be done by + // taking the delta relative to that one parent and redoing + // that on the current merge tip. + // + m.setBase(n.getParent(0)); + if (m.merge(newMergeTip, n)) { + newMergeTip = writeCherryPickCommit(m, newMergeTip, n); + + } else { + n.statusCode = CommitMergeStatus.PATH_CONFLICT; + } + + } else { + // There are multiple parents, so this is a merge commit. We + // don't want to cherry-pick the merge as clients can't easily + // rebase their history with that merge present and replaced + // by an equivalent merge with a different first parent. So + // instead behave as though MERGE_IF_NECESSARY was configured. + // + if (!hasMissingDependencies(args.mergeSorter, n)) { + if (args.rw.isMergedInto(newMergeTip, n)) { + newMergeTip = n; + } else { + newMergeTip = + mergeOneCommit(args.db, args.identifiedUserFactory, + args.myIdent, args.repo, args.rw, args.inserter, + args.canMergeFlag, args.useContentMerge, args.destBranch, + newMergeTip, n); + } + final PatchSetApproval submitApproval = + markCleanMerges(args.db, args.rw, args.canMergeFlag, + newMergeTip, args.alreadyAccepted); + setRefLogIdent(submitApproval); + + } else { + // One or more dependencies were not met. The status was + // already marked on the commit so we have nothing further + // to perform at this time. + // + } + } + + } catch (IOException e) { + throw new MergeException("Cannot merge " + n.name(), e); + } catch (OrmException e) { + throw new MergeException("Cannot merge " + n.name(), e); + } + } + return newMergeTip; + } + + @Override + public Map getNewCommits() { + return newCommits; + } + + private CodeReviewCommit writeCherryPickCommit(final Merger m, + final CodeReviewCommit mergeTip, final CodeReviewCommit n) + throws IOException, OrmException { + args.rw.parseBody(n); + + final List footers = n.getFooterLines(); + final StringBuilder msgbuf = new StringBuilder(); + msgbuf.append(n.getFullMessage()); + + if (msgbuf.length() == 0) { + // WTF, an empty commit message? + msgbuf.append(""); + } + if (msgbuf.charAt(msgbuf.length() - 1) != '\n') { + // Missing a trailing LF? Correct it (perhaps the editor was broken). + msgbuf.append('\n'); + } + if (footers.isEmpty()) { + // Doesn't end in a "Signed-off-by: ..." style line? Add another line + // break to start a new paragraph for the reviewed-by tag lines. + // + msgbuf.append('\n'); + } + + if (!contains(footers, CHANGE_ID, n.change.getKey().get())) { + msgbuf.append(CHANGE_ID.getName()); + msgbuf.append(": "); + msgbuf.append(n.change.getKey().get()); + msgbuf.append('\n'); + } + + final String siteUrl = urlProvider.get(); + if (siteUrl != null) { + final String url = siteUrl + n.patchsetId.getParentKey().get(); + if (!contains(footers, REVIEWED_ON, url)) { + msgbuf.append(REVIEWED_ON.getName()); + msgbuf.append(": "); + msgbuf.append(url); + msgbuf.append('\n'); + } + } + + PatchSetApproval submitAudit = null; + List approvalList = null; + try { + approvalList = + args.db.patchSetApprovals().byPatchSet(n.patchsetId).toList(); + Collections.sort(approvalList, new Comparator() { + @Override + public int compare(final PatchSetApproval a, final PatchSetApproval b) { + return a.getGranted().compareTo(b.getGranted()); + } + }); + + for (final PatchSetApproval a : approvalList) { + if (a.getValue() <= 0) { + // Negative votes aren't counted. + continue; + } + + if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) { + // Submit is treated specially, below (becomes committer) + // + if (submitAudit == null + || a.getGranted().compareTo(submitAudit.getGranted()) > 0) { + submitAudit = a; + } + continue; + } + + final Account acc = + args.identifiedUserFactory.create(a.getAccountId()).getAccount(); + final StringBuilder identbuf = new StringBuilder(); + if (acc.getFullName() != null && acc.getFullName().length() > 0) { + if (identbuf.length() > 0) { + identbuf.append(' '); + } + identbuf.append(acc.getFullName()); + } + if (acc.getPreferredEmail() != null + && acc.getPreferredEmail().length() > 0) { + if (isSignedOffBy(footers, acc.getPreferredEmail())) { + continue; + } + if (identbuf.length() > 0) { + identbuf.append(' '); + } + identbuf.append('<'); + identbuf.append(acc.getPreferredEmail()); + identbuf.append('>'); + } + if (identbuf.length() == 0) { + // Nothing reasonable to describe them by? Ignore them. + continue; + } + + final String tag; + if (CRVW.equals(a.getCategoryId())) { + tag = "Reviewed-by"; + } else if (VRIF.equals(a.getCategoryId())) { + tag = "Tested-by"; + } else { + final ApprovalType at = approvalTypes.byId(a.getCategoryId()); + if (at == null) { + // A deprecated/deleted approval type, ignore it. + continue; + } + tag = at.getCategory().getName().replace(' ', '-'); + } + + if (!contains(footers, new FooterKey(tag), identbuf.toString())) { + msgbuf.append(tag); + msgbuf.append(": "); + msgbuf.append(identbuf); + msgbuf.append('\n'); + } + } + } catch (OrmException e) { + log.error("Can't read approval records for " + n.patchsetId, e); + } + + final CommitBuilder mergeCommit = new CommitBuilder(); + mergeCommit.setTreeId(m.getResultTreeId()); + mergeCommit.setParentId(mergeTip); + mergeCommit.setAuthor(n.getAuthorIdent()); + mergeCommit.setCommitter(toCommitterIdent(submitAudit)); + mergeCommit.setMessage(msgbuf.toString()); + + final ObjectId id = commit(args.inserter, mergeCommit); + final CodeReviewCommit newCommit = + (CodeReviewCommit) args.rw.parseCommit(id); + + n.change.nextPatchSetId(); + + final PatchSet ps = new PatchSet(n.change.currPatchSetId()); + ps.setCreatedOn(new Timestamp(System.currentTimeMillis())); + ps.setUploader(submitAudit.getAccountId()); + ps.setRevision(new RevId(id.getName())); + insertAncestors(ps.getId(), newCommit); + args.db.patchSets().insert(Collections.singleton(ps)); + + n.change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId())); + args.db.changes().update(Collections.singletonList(n.change)); + + if (approvalList != null) { + for (PatchSetApproval a : approvalList) { + args.db.patchSetApprovals().insert( + Collections.singleton(new PatchSetApproval(ps.getId(), a))); + } + } + + final RefUpdate ru = args.repo.updateRef(ps.getRefName()); + ru.setExpectedOldObjectId(ObjectId.zeroId()); + ru.setNewObjectId(newCommit); + ru.disableRefLog(); + if (ru.update(args.rw) != RefUpdate.Result.NEW) { + throw new IOException(String.format("Failed to create ref %s in %s: %s", + ps.getRefName(), n.change.getDest().getParentKey().get(), + ru.getResult())); + } + replication.fire(n.change.getProject(), ru.getName()); + + newCommit.copyFrom(n); + newCommit.statusCode = CommitMergeStatus.CLEAN_PICK; + newCommits.put(newCommit.patchsetId.getParentKey(), newCommit); + setRefLogIdent(submitAudit); + return newCommit; + } + + private void insertAncestors(PatchSet.Id id, RevCommit src) + throws OrmException { + final int cnt = src.getParentCount(); + List toInsert = new ArrayList(cnt); + for (int p = 0; p < cnt; p++) { + PatchSetAncestor a; + + a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1)); + a.setAncestorRevision(new RevId(src.getParent(p).getId().name())); + toInsert.add(a); + } + args.db.patchSetAncestors().insert(toInsert); + } + + private boolean contains(List footers, FooterKey key, String val) { + for (final FooterLine line : footers) { + if (line.matches(key) && val.equals(line.getValue())) { + return true; + } + } + return false; + } + + private boolean isSignedOffBy(List footers, String email) { + for (final FooterLine line : footers) { + if (line.matches(FooterKey.SIGNED_OFF_BY) + && email.equals(line.getEmailAddress())) { + return true; + } + } + return false; + } + + private PersonIdent toCommitterIdent(final PatchSetApproval audit) { + if (audit != null) { + return args.identifiedUserFactory.create(audit.getAccountId()) + .newCommitterIdent(audit.getGranted(), args.myIdent.getTimeZone()); + } + return args.myIdent; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java index 87903511fc..512944f0a2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java @@ -38,6 +38,9 @@ enum CommitMergeStatus { /** */ REVISION_GONE(""), + /** */ + NO_SUBMIT_TYPE(""), + /** */ CRISS_CROSS_MERGE("Your change requires a recursive merge to resolve.\n" + "\n" diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/FastForwardOnly.java new file mode 100644 index 0000000000..ab02bf99e2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/FastForwardOnly.java @@ -0,0 +1,55 @@ +// Copyright (C) 2012 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.git; + +import static com.google.gerrit.server.git.MergeUtil.getFirstFastForward; +import static com.google.gerrit.server.git.MergeUtil.markCleanMerges; +import static com.google.gerrit.server.git.MergeUtil.reduceToMinimalMerge; + +import com.google.gerrit.reviewdb.client.PatchSetApproval; + +import java.util.List; + +public class FastForwardOnly extends SubmitStrategy { + + FastForwardOnly(final SubmitStrategy.Arguments args) { + super(args); + } + + @Override + protected CodeReviewCommit _run(final CodeReviewCommit mergeTip, + final List toMerge) throws MergeException { + reduceToMinimalMerge(args.mergeSorter, toMerge); + final CodeReviewCommit newMergeTip = + getFirstFastForward(mergeTip, args.rw, toMerge); + + while (!toMerge.isEmpty()) { + final CodeReviewCommit n = toMerge.remove(0); + n.statusCode = CommitMergeStatus.NOT_FAST_FORWARD; + } + + final PatchSetApproval submitApproval = + markCleanMerges(args.db, args.rw, args.canMergeFlag, newMergeTip, + args.alreadyAccepted); + setRefLogIdent(submitApproval); + + return newMergeTip; + } + + @Override + public boolean retryOnLockFailure() { + return false; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java new file mode 100644 index 0000000000..2840d3cb28 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java @@ -0,0 +1,52 @@ +// Copyright (C) 2012 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.git; + +import static com.google.gerrit.server.git.MergeUtil.markCleanMerges; +import static com.google.gerrit.server.git.MergeUtil.mergeOneCommit; +import static com.google.gerrit.server.git.MergeUtil.reduceToMinimalMerge; + +import com.google.gerrit.reviewdb.client.PatchSetApproval; + +import java.util.List; + +public class MergeAlways extends SubmitStrategy { + + MergeAlways(final SubmitStrategy.Arguments args) { + super(args); + } + + @Override + protected CodeReviewCommit _run(final CodeReviewCommit mergeTip, + final List toMerge) throws MergeException { + reduceToMinimalMerge(args.mergeSorter, toMerge); + + CodeReviewCommit newMergeTip = mergeTip; + while (!toMerge.isEmpty()) { + newMergeTip = + mergeOneCommit(args.db, args.identifiedUserFactory, args.myIdent, + args.repo, args.rw, args.inserter, args.canMergeFlag, + args.useContentMerge, args.destBranch, mergeTip, + toMerge.remove(0)); + } + + final PatchSetApproval submitApproval = + markCleanMerges(args.db, args.rw, args.canMergeFlag, newMergeTip, + args.alreadyAccepted); + setRefLogIdent(submitApproval); + + return newMergeTip; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java new file mode 100644 index 0000000000..34811b889e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java @@ -0,0 +1,55 @@ +// Copyright (C) 2012 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.git; + +import static com.google.gerrit.server.git.MergeUtil.getFirstFastForward; +import static com.google.gerrit.server.git.MergeUtil.markCleanMerges; +import static com.google.gerrit.server.git.MergeUtil.mergeOneCommit; +import static com.google.gerrit.server.git.MergeUtil.reduceToMinimalMerge; + +import com.google.gerrit.reviewdb.client.PatchSetApproval; + +import java.util.List; + +public class MergeIfNecessary extends SubmitStrategy { + + MergeIfNecessary(final SubmitStrategy.Arguments args) { + super(args); + } + + @Override + protected CodeReviewCommit _run(final CodeReviewCommit mergeTip, + final List toMerge) throws MergeException { + reduceToMinimalMerge(args.mergeSorter, toMerge); + CodeReviewCommit newMergeTip = + getFirstFastForward(mergeTip, args.rw, toMerge); + + // For every other commit do a pair-wise merge. + while (!toMerge.isEmpty()) { + newMergeTip = + mergeOneCommit(args.db, args.identifiedUserFactory, args.myIdent, + args.repo, args.rw, args.inserter, args.canMergeFlag, + args.useContentMerge, args.destBranch, mergeTip, + toMerge.remove(0)); + } + + final PatchSetApproval submitApproval = + markCleanMerges(args.db, args.rw, args.canMergeFlag, newMergeTip, + args.alreadyAccepted); + setRefLogIdent(submitApproval); + + return newMergeTip; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java index ff92e83839..612e3fbb50 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java @@ -14,6 +14,8 @@ package com.google.gerrit.server.git; +import static com.google.gerrit.server.git.MergeUtil.computeMergeCommitAuthor; +import static com.google.gerrit.server.git.MergeUtil.getSubmitter; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; @@ -21,22 +23,22 @@ import com.google.gerrit.common.ChangeHooks; import com.google.gerrit.common.data.ApprovalType; import com.google.gerrit.common.data.ApprovalTypes; import com.google.gerrit.common.data.Capable; +import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.ApprovalCategory; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.reviewdb.client.PatchSetAncestor; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.Project.SubmitType; import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.mail.MergeFailSender; import com.google.gerrit.server.mail.MergedSender; @@ -54,14 +56,11 @@ import com.google.gwtorm.server.OrmConcurrencyException; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.AnyObjectId; -import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; @@ -69,11 +68,6 @@ 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.MergeStrategy; -import org.eclipse.jgit.merge.Merger; -import org.eclipse.jgit.merge.ThreeWayMerger; -import org.eclipse.jgit.revwalk.FooterKey; -import org.eclipse.jgit.revwalk.FooterLine; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevSort; @@ -82,22 +76,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Map.Entry; import java.util.Set; -import java.util.TimeZone; - -import javax.annotation.Nullable; /** * Merges changes in submission order into a single branch. @@ -119,14 +107,6 @@ public class MergeOp { } private static final Logger log = LoggerFactory.getLogger(MergeOp.class); - private static final String R_HEADS_MASTER = - Constants.R_HEADS + Constants.MASTER; - private static final ApprovalCategory.Id CRVW = - new ApprovalCategory.Id("CRVW"); - private static final ApprovalCategory.Id VRIF = - new ApprovalCategory.Id("VRIF"); - private static final FooterKey REVIEWED_ON = new FooterKey("Reviewed-on"); - private static final FooterKey CHANGE_ID = new FooterKey("Change-Id"); /** Amount of time to wait between submit and checking for missing deps. */ private static final long DEPENDENCY_DELAY = @@ -141,7 +121,6 @@ public class MergeOp { private final GitReferenceUpdated replication; private final MergedSender.Factory mergedSenderFactory; private final MergeFailSender.Factory mergeFailSenderFactory; - private final Provider urlProvider; private final ApprovalTypes approvalTypes; private final PatchSetInfoFactory patchSetInfoFactory; private final IdentifiedUser.GenericFactory identifiedUserFactory; @@ -151,23 +130,23 @@ public class MergeOp { private final PersonIdent myIdent; private final Branch.NameKey destBranch; private Project destProject; - private final List toMerge; - private List submitted; + private final Map> toMerge; + private final List potentiallyStillSubmittable; private final Map commits; private ReviewDb db; private Repository repo; private RevWalk rw; - private RevFlag CAN_MERGE; + private RevFlag canMergeFlag; private CodeReviewCommit branchTip; private CodeReviewCommit mergeTip; - private Set alreadyAccepted; - private RefUpdate branchUpdate; private ObjectInserter inserter; + private PersonIdent refLogIdent; private final ChangeHooks hooks; private final AccountCache accountCache; private final TagCache tagCache; private final CreateCodeReviewNotes.Factory codeReviewNotesFactory; + private final SubmitStrategyFactory submitStrategyFactory; private final SubmoduleOp.Factory subOpFactory; private final WorkQueue workQueue; private final RequestScopePropagator requestScopePropagator; @@ -177,7 +156,6 @@ public class MergeOp { final ProjectCache pc, final FunctionState.Factory fs, final GitReferenceUpdated rq, final MergedSender.Factory msf, final MergeFailSender.Factory mfsf, - @CanonicalWebUrl @Nullable final Provider cwu, final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif, final IdentifiedUser.GenericFactory iuf, final ChangeControl.GenericFactory changeControlFactory, @@ -185,6 +163,7 @@ public class MergeOp { final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch, final ChangeHooks hooks, final AccountCache accountCache, final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf, + final SubmitStrategyFactory submitStrategyFactory, final SubmoduleOp.Factory subOpFactory, final WorkQueue workQueue, final RequestScopePropagator requestScopePropagator) { @@ -195,7 +174,6 @@ public class MergeOp { replication = rq; mergedSenderFactory = msf; mergeFailSenderFactory = mfsf; - urlProvider = cwu; this.approvalTypes = approvalTypes; patchSetInfoFactory = psif; identifiedUserFactory = iuf; @@ -205,12 +183,14 @@ public class MergeOp { this.accountCache = accountCache; this.tagCache = tagCache; codeReviewNotesFactory = crnf; + this.submitStrategyFactory = submitStrategyFactory; this.subOpFactory = subOpFactory; this.workQueue = workQueue; this.requestScopePropagator = requestScopePropagator; this.myIdent = myIdent; destBranch = branch; - toMerge = new ArrayList(); + toMerge = new HashMap>(); + potentiallyStillSubmittable = new ArrayList(); commits = new HashMap(); } @@ -219,8 +199,6 @@ public class MergeOp { setDestProject(); openRepository(); final Ref destBranchRef = repo.getRef(destBranch.get()); - submitted = new ArrayList(); - submitted.add(change); // Test mergeability of the change if the last merged sha1 // in the branch is different from the last sha1 @@ -230,7 +208,11 @@ public class MergeOp { || (destBranchRef != null && !destBranchRef.getObjectId().getName() .equals(change.getLastSha1MergeTested().get()))) { openSchema(); - preMerge(); + openBranch(); + validateChangeList(Collections.singletonList(change)); + final Entry> entry = + toMerge.entrySet().iterator().next(); + preMerge(createStrategy(entry.getKey()), entry.getValue()); // update sha1 tested merge. if (destBranchRef != null) { @@ -280,11 +262,53 @@ public class MergeOp { try { openSchema(); openRepository(); - submitted = db.changes().submitted(destBranch).toList(); - preMerge(); - updateBranch(); - updateChangeStatus(); - updateSubscriptions(); + openBranch(); + final Map> toSubmit = + validateChangeList(db.changes().submitted(destBranch).toList()); + + final Map> toMergeNextTurn = + new HashMap>(); + final List potentiallyStillSubmittableOnNextRun = + new ArrayList(); + while (!toMerge.isEmpty()) { + toMergeNextTurn.clear(); + for (final Entry> e : toMerge.entrySet()) { + final SubmitType submitType = e.getKey(); + final RefUpdate branchUpdate = openBranch(); + final SubmitStrategy strategy = createStrategy(submitType); + preMerge(strategy, e.getValue()); + updateBranch(strategy, branchUpdate); + updateChangeStatus(toSubmit.get(submitType)); + updateSubscriptions(toSubmit.get(submitType)); + + for (final Iterator it = + potentiallyStillSubmittable.iterator(); it.hasNext();) { + final CodeReviewCommit commit = it.next(); + if (containsMissingCommits(toMerge, commit) + || containsMissingCommits(toMergeNextTurn, commit)) { + // change has missing dependencies, but all commits which are + // missing are still attempted to be merged with another submit + // strategy, retry to merge this commit in the next turn + it.remove(); + commit.statusCode = null; + commit.missing = null; + getList(submitType, toMergeNextTurn).add(commit); + } + } + potentiallyStillSubmittableOnNextRun.addAll(potentiallyStillSubmittable); + potentiallyStillSubmittable.clear(); + } + toMerge.clear(); + toMerge.putAll(toMergeNextTurn); + } + + for (final CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) { + final Capable capable = isSubmitStillPossible(commit); + if (capable != Capable.OK) { + sendMergeFail(commit.change, + message(commit.change, capable.getMessage()), false); + } + } } catch (OrmException e) { throw new MergeException("Cannot query the database", e); } finally { @@ -303,24 +327,66 @@ public class MergeOp { } } - private void preMerge() throws MergeException, OrmException { - openBranch(); - validateChangeList(); - mergeTip = branchTip; - switch (destProject.getSubmitType()) { - case CHERRY_PICK: - cherryPickChanges(); - break; - - case FAST_FORWARD_ONLY: - case MERGE_ALWAYS: - case MERGE_IF_NECESSARY: - default: - reduceToMinimalMerge(); - mergeTopics(); - markCleanMerges(); - break; + private boolean containsMissingCommits( + final Map> map, + final CodeReviewCommit commit) { + if (!isSubmitForMissingCommitsStillPossible(commit)) { + return false; } + + for (final CodeReviewCommit missingCommit : commit.missing) { + boolean found = false; + for (final List list : map.values()) { + if (list.contains(missingCommit)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + private boolean isSubmitForMissingCommitsStillPossible(final CodeReviewCommit commit) { + if (commit.missing == null || commit.missing.isEmpty()) { + return false; + } + + for (CodeReviewCommit missingCommit : commit.missing) { + loadChangeInfo(missingCommit); + + if (missingCommit.patchsetId == null) { + // The commit doesn't have a patch set, so it cannot be + // submitted to the branch. + // + return false; + } + + if (!missingCommit.change.currentPatchSetId().equals( + missingCommit.patchsetId)) { + // If the missing commit is not the current patch set, + // the change must be rebased to use the proper parent. + // + return false; + } + } + + return true; + } + + private void preMerge(final SubmitStrategy strategy, + final List toMerge) throws MergeException { + mergeTip = strategy.run(branchTip, toMerge); + refLogIdent = strategy.getRefLogIdent(); + commits.putAll(strategy.getNewCommits()); + } + + private SubmitStrategy createStrategy(final SubmitType submitType) throws MergeException { + return submitStrategyFactory.create(submitType, db, repo, rw, inserter, + canMergeFlag, getAlreadyAccepted(branchTip), destBranch, + destProject.isUseContentMerge()); } private void openRepository() throws MergeException { @@ -343,20 +409,17 @@ public class MergeOp { }; rw.sort(RevSort.TOPO); rw.sort(RevSort.COMMIT_TIME_DESC, true); - CAN_MERGE = rw.newFlag("CAN_MERGE"); + canMergeFlag = rw.newFlag("CAN_MERGE"); inserter = repo.newObjectInserter(); } - private void openBranch() throws MergeException { - alreadyAccepted = new HashSet(); - + private RefUpdate openBranch() throws MergeException { try { - branchUpdate = repo.updateRef(destBranch.get()); + final RefUpdate branchUpdate = repo.updateRef(destBranch.get()); if (branchUpdate.getOldObjectId() != null) { branchTip = (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId()); - alreadyAccepted.add(branchTip); } else { branchTip = null; } @@ -376,6 +439,21 @@ public class MergeOp { "Failed to check existence of destination branch", e); } + return branchUpdate; + } catch (IOException e) { + throw new MergeException("Cannot open branch", e); + } + } + + private Set getAlreadyAccepted(final CodeReviewCommit branchTip) + throws MergeException { + final Set alreadyAccepted = new HashSet(); + + if (branchTip != null) { + alreadyAccepted.add(branchTip); + } + + try { for (final Ref r : repo.getAllRefs().values()) { if (r.getName().startsWith(Constants.R_HEADS) || r.getName().startsWith(Constants.R_TAGS)) { @@ -387,11 +465,17 @@ public class MergeOp { } } } catch (IOException e) { - throw new MergeException("Cannot open branch", e); + throw new MergeException("Failed to determine already accepted commits.", e); } + + return alreadyAccepted; } - private void validateChangeList() throws MergeException { + private Map> validateChangeList( + final List submitted) throws MergeException { + final Map> toSubmit = + new HashMap>(); + final Set tips = new HashSet(); for (final Ref r : repo.getAllRefs().values()) { tips.add(r.getObjectId()); @@ -461,7 +545,7 @@ public class MergeOp { 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 + // that we got back here. Just mark it complete and move on. It's // merged and that is all that mattered to the requestor. // try { @@ -474,580 +558,53 @@ public class MergeOp { } } - commit.add(CAN_MERGE); - toMerge.add(commit); + final SubmitType submitType = getSubmitType(chg, ps); + if (submitType == null) { + commits.put(changeId, + CodeReviewCommit.error(CommitMergeStatus.NO_SUBMIT_TYPE)); + continue; + } + + commit.add(canMergeFlag); + getList(submitType, toMerge).add(commit); + getList(submitType, toSubmit).add(chg); } + return toSubmit; } - private void reduceToMinimalMerge() throws MergeException { - final Collection heads; + private SubmitType getSubmitType(final Change change, final PatchSet ps) { try { - heads = new MergeSorter(rw, alreadyAccepted, CAN_MERGE).sort(toMerge); - } catch (IOException e) { - throw new MergeException("Branch head sorting failed", e); - } - - toMerge.clear(); - toMerge.addAll(heads); - Collections.sort(toMerge, new Comparator() { - @Override - public int compare(final CodeReviewCommit a, final CodeReviewCommit b) { - return a.originalOrder - b.originalOrder; - } - }); - } - - private void mergeTopics() throws MergeException { - // Take the first fast-forward available, if any is available in the set. - // - if (destProject.getSubmitType() != Project.SubmitType.MERGE_ALWAYS) { - for (final Iterator i = toMerge.iterator(); i.hasNext();) { - try { - final CodeReviewCommit n = i.next(); - if (mergeTip == null || rw.isMergedInto(mergeTip, n)) { - mergeTip = n; - i.remove(); - break; - } - } catch (IOException e) { - throw new MergeException("Cannot fast-forward test during merge", e); - } - } - } - - if (destProject.getSubmitType() == Project.SubmitType.FAST_FORWARD_ONLY) { - // If this project only permits fast-forwards, abort everything else. - // - while (!toMerge.isEmpty()) { - final CodeReviewCommit n = toMerge.remove(0); - n.statusCode = CommitMergeStatus.NOT_FAST_FORWARD; - } - - } else { - // For every other commit do a pair-wise merge. - // - while (!toMerge.isEmpty()) { - mergeOneCommit(toMerge.remove(0)); + final SubmitTypeRecord r = + changeControlFactory.controlFor(change, + identifiedUserFactory.create(change.getOwner())) + .getSubmitTypeRecord(db, ps); + if (r.status != SubmitTypeRecord.Status.OK) { + log.error("Failed to get submit type for " + change.getKey()); + return null; } + return r.type; + } catch (NoSuchChangeException e) { + log.error("Failed to get submit type for " + change.getKey(), e); + return null; } } - private void mergeOneCommit(final CodeReviewCommit n) throws MergeException { - final ThreeWayMerger m = newThreeWayMerger(); - try { - if (m.merge(new AnyObjectId[] {mergeTip, n})) { - writeMergeCommit(m.getResultTreeId(), n); - - } else { - failed(n, CommitMergeStatus.PATH_CONFLICT); - } - } catch (IOException e) { - if (e.getMessage().startsWith("Multiple merge bases for")) { - try { - failed(n, CommitMergeStatus.CRISS_CROSS_MERGE); - } catch (IOException e2) { - throw new MergeException("Cannot merge " + n.name(), e); - } - } else { - throw new MergeException("Cannot merge " + n.name(), e); - } + private static List getList(final K key, final Map> map) { + List list = map.get(key); + if (list == null) { + list = new ArrayList(); + map.put(key, list); } + return list; } - private ThreeWayMerger newThreeWayMerger() { - ThreeWayMerger m; - if (destProject.isUseContentMerge()) { - // Settings for this project allow us to try and - // automatically resolve conflicts within files if needed. - // Use ResolveMerge and instruct to operate in core. - m = MergeStrategy.RESOLVE.newMerger(repo, true); - } else { - // No auto conflict resolving allowed. If any of the - // affected files was modified, merge will fail. - m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo); - } - m.setObjectInserter(new ObjectInserter.Filter() { - @Override - protected ObjectInserter delegate() { - return inserter; - } - - @Override - public void flush() { - } - - @Override - public void release() { - } - }); - return m; - } - - private CodeReviewCommit failed(final CodeReviewCommit n, - final CommitMergeStatus failure) throws MissingObjectException, - IncorrectObjectTypeException, IOException { - rw.reset(); - rw.markStart(n); - rw.markUninteresting(mergeTip); - CodeReviewCommit failed; - while ((failed = (CodeReviewCommit) rw.next()) != null) { - failed.statusCode = failure; - } - return failed; - } - - private void writeMergeCommit(ObjectId treeId, CodeReviewCommit n) - throws IOException, MissingObjectException, IncorrectObjectTypeException { - final List merged = new ArrayList(); - rw.reset(); - rw.markStart(n); - rw.markUninteresting(mergeTip); - for (final RevCommit c : rw) { - final CodeReviewCommit crc = (CodeReviewCommit) c; - if (crc.patchsetId != null) { - merged.add(crc); - } - } - - final StringBuilder msgbuf = new StringBuilder(); - if (merged.size() == 1) { - final CodeReviewCommit c = merged.get(0); - rw.parseBody(c); - msgbuf.append("Merge \""); - msgbuf.append(c.getShortMessage()); - msgbuf.append("\""); - - } else { - msgbuf.append("Merge changes "); - for (final Iterator i = merged.iterator(); i.hasNext();) { - msgbuf.append(i.next().change.getKey().abbreviate()); - if (i.hasNext()) { - msgbuf.append(','); - } - } - } - - if (!R_HEADS_MASTER.equals(destBranch.get())) { - msgbuf.append(" into "); - msgbuf.append(destBranch.getShortName()); - } - - if (merged.size() > 1) { - msgbuf.append("\n\n* changes:\n"); - for (final CodeReviewCommit c : merged) { - rw.parseBody(c); - msgbuf.append(" "); - msgbuf.append(c.getShortMessage()); - msgbuf.append("\n"); - } - } - - PersonIdent authorIdent = computeAuthor(merged); - - final CommitBuilder mergeCommit = new CommitBuilder(); - mergeCommit.setTreeId(treeId); - mergeCommit.setParentIds(mergeTip, n); - mergeCommit.setAuthor(authorIdent); - mergeCommit.setCommitter(myIdent); - mergeCommit.setMessage(msgbuf.toString()); - - mergeTip = (CodeReviewCommit) rw.parseCommit(commit(mergeCommit)); - } - - private PersonIdent computeAuthor( - final List codeReviewCommits) { - PatchSetApproval submitter = null; - for (final CodeReviewCommit c : codeReviewCommits) { - PatchSetApproval s = getSubmitter(db, c.patchsetId); - if (submitter == null - || (s != null && s.getGranted().compareTo(submitter.getGranted()) > 0)) { - submitter = s; - } - } - - // Try to use the submitter's identity for the merge commit author. - // If all of the commits being merged are created by the submitter, - // prefer the identity line they used in the commits rather than the - // preferred identity stored in the user account. This way the Git - // commit records are more consistent internally. - // - PersonIdent authorIdent; - if (submitter != null) { - IdentifiedUser who = - identifiedUserFactory.create(submitter.getAccountId()); - Set emails = new HashSet(); - for (RevCommit c : codeReviewCommits) { - try { - rw.parseBody(c); - } catch (IOException e) { - log.warn("Cannot parse commit " + c.name() + " in " + destBranch, e); - continue; - } - emails.add(c.getAuthorIdent().getEmailAddress()); - } - - final Timestamp dt = submitter.getGranted(); - final TimeZone tz = myIdent.getTimeZone(); - if (emails.size() == 1 - && who.getEmailAddresses().contains(emails.iterator().next())) { - authorIdent = - new PersonIdent(codeReviewCommits.get(0).getAuthorIdent(), dt, tz); - } else { - authorIdent = who.newCommitterIdent(dt, tz); - } - } else { - authorIdent = myIdent; - } - return authorIdent; - } - - private void markCleanMerges() throws MergeException { - if (mergeTip == null) { - // If mergeTip is null here, branchTip was null, indicating a new branch - // at the start of the merge process. We also elected to merge nothing, - // probably due to missing dependencies. Nothing was cleanly merged. - // + private void updateBranch(final SubmitStrategy strategy, + final RefUpdate branchUpdate) throws MergeException { + if ((branchTip == null && mergeTip == null) || branchTip == mergeTip) { + // nothing to do return; } - try { - rw.reset(); - rw.sort(RevSort.TOPO); - rw.sort(RevSort.REVERSE, true); - rw.markStart(mergeTip); - for (RevCommit c : alreadyAccepted) { - rw.markUninteresting(c); - } - - CodeReviewCommit c; - while ((c = (CodeReviewCommit) rw.next()) != null) { - if (c.patchsetId != null) { - c.statusCode = CommitMergeStatus.CLEAN_MERGE; - if (branchUpdate.getRefLogIdent() == null) { - setRefLogIdent(getSubmitter(db, c.patchsetId)); - } - } - } - } catch (IOException e) { - throw new MergeException("Cannot mark clean merges", e); - } - } - - private void setRefLogIdent(final PatchSetApproval submitAudit) { - if (submitAudit != null) { - branchUpdate.setRefLogIdent(identifiedUserFactory.create( - submitAudit.getAccountId()).newRefLogIdent()); - } - } - - private void cherryPickChanges() throws MergeException, OrmException { - while (!toMerge.isEmpty()) { - final CodeReviewCommit n = toMerge.remove(0); - final ThreeWayMerger m = newThreeWayMerger(); - try { - if (mergeTip == null) { - // The branch is unborn. Take a fast-forward resolution to - // create the branch. - // - mergeTip = n; - n.statusCode = CommitMergeStatus.CLEAN_MERGE; - - } else if (n.getParentCount() == 0) { - // Refuse to merge a root commit into an existing branch, - // we cannot obtain a delta for the cherry-pick to apply. - // - n.statusCode = CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT; - - } else if (n.getParentCount() == 1) { - // If there is only one parent, a cherry-pick can be done by - // taking the delta relative to that one parent and redoing - // that on the current merge tip. - // - m.setBase(n.getParent(0)); - if (m.merge(mergeTip, n)) { - writeCherryPickCommit(m, n); - - } else { - n.statusCode = CommitMergeStatus.PATH_CONFLICT; - } - - } else { - // There are multiple parents, so this is a merge commit. We - // don't want to cherry-pick the merge as clients can't easily - // rebase their history with that merge present and replaced - // by an equivalent merge with a different first parent. So - // instead behave as though MERGE_IF_NECESSARY was configured. - // - if (hasDependenciesMet(n)) { - if (rw.isMergedInto(mergeTip, n)) { - mergeTip = n; - } else { - mergeOneCommit(n); - } - markCleanMerges(); - - } else { - // One or more dependencies were not met. The status was - // already marked on the commit so we have nothing further - // to perform at this time. - // - } - } - - } catch (IOException e) { - throw new MergeException("Cannot merge " + n.name(), e); - } - } - } - - private boolean hasDependenciesMet(final CodeReviewCommit n) - throws IOException { - // Oddly we can determine this by running the merge sorter and - // look for the one commit to come out as a result. This works - // as the merge sorter checks the dependency chain as part of - // its logic trying to find a minimal merge path. - // - return new MergeSorter(rw, alreadyAccepted, CAN_MERGE).sort( - Collections.singleton(n)).contains(n); - } - - private void writeCherryPickCommit(final Merger m, final CodeReviewCommit n) - throws IOException, OrmException { - rw.parseBody(n); - - final List footers = n.getFooterLines(); - final StringBuilder msgbuf = new StringBuilder(); - msgbuf.append(n.getFullMessage()); - - if (msgbuf.length() == 0) { - // WTF, an empty commit message? - msgbuf.append(""); - } - if (msgbuf.charAt(msgbuf.length() - 1) != '\n') { - // Missing a trailing LF? Correct it (perhaps the editor was broken). - msgbuf.append('\n'); - } - if (footers.isEmpty()) { - // Doesn't end in a "Signed-off-by: ..." style line? Add another line - // break to start a new paragraph for the reviewed-by tag lines. - // - msgbuf.append('\n'); - } - - if (!contains(footers, CHANGE_ID, n.change.getKey().get())) { - msgbuf.append(CHANGE_ID.getName()); - msgbuf.append(": "); - msgbuf.append(n.change.getKey().get()); - msgbuf.append('\n'); - } - - final String siteUrl = urlProvider.get(); - if (siteUrl != null) { - final String url = siteUrl + n.patchsetId.getParentKey().get(); - if (!contains(footers, REVIEWED_ON, url)) { - msgbuf.append(REVIEWED_ON.getName()); - msgbuf.append(": "); - msgbuf.append(url); - msgbuf.append('\n'); - } - } - - PatchSetApproval submitAudit = null; - List approvalList = null; - try { - approvalList = - db.patchSetApprovals().byPatchSet(n.patchsetId).toList(); - Collections.sort(approvalList, new Comparator() { - @Override - public int compare(final PatchSetApproval a, final PatchSetApproval b) { - return a.getGranted().compareTo(b.getGranted()); - } - }); - - for (final PatchSetApproval a : approvalList) { - if (a.getValue() <= 0) { - // Negative votes aren't counted. - continue; - } - - if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) { - // Submit is treated specially, below (becomes committer) - // - if (submitAudit == null - || a.getGranted().compareTo(submitAudit.getGranted()) > 0) { - submitAudit = a; - } - continue; - } - - final Account acc = - identifiedUserFactory.create(a.getAccountId()).getAccount(); - final StringBuilder identbuf = new StringBuilder(); - if (acc.getFullName() != null && acc.getFullName().length() > 0) { - if (identbuf.length() > 0) { - identbuf.append(' '); - } - identbuf.append(acc.getFullName()); - } - if (acc.getPreferredEmail() != null - && acc.getPreferredEmail().length() > 0) { - if (isSignedOffBy(footers, acc.getPreferredEmail())) { - continue; - } - if (identbuf.length() > 0) { - identbuf.append(' '); - } - identbuf.append('<'); - identbuf.append(acc.getPreferredEmail()); - identbuf.append('>'); - } - if (identbuf.length() == 0) { - // Nothing reasonable to describe them by? Ignore them. - continue; - } - - final String tag; - if (CRVW.equals(a.getCategoryId())) { - tag = "Reviewed-by"; - } else if (VRIF.equals(a.getCategoryId())) { - tag = "Tested-by"; - } else { - final ApprovalType at = - approvalTypes.byId(a.getCategoryId()); - if (at == null) { - // A deprecated/deleted approval type, ignore it. - continue; - } - tag = at.getCategory().getName().replace(' ', '-'); - } - - if (!contains(footers, new FooterKey(tag), identbuf.toString())) { - msgbuf.append(tag); - msgbuf.append(": "); - msgbuf.append(identbuf); - msgbuf.append('\n'); - } - } - } catch (OrmException e) { - log.error("Can't read approval records for " + n.patchsetId, e); - } - - final CommitBuilder mergeCommit = new CommitBuilder(); - mergeCommit.setTreeId(m.getResultTreeId()); - mergeCommit.setParentId(mergeTip); - mergeCommit.setAuthor(n.getAuthorIdent()); - mergeCommit.setCommitter(toCommitterIdent(submitAudit)); - mergeCommit.setMessage(msgbuf.toString()); - - final ObjectId id = commit(mergeCommit); - final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id); - final Change oldChange = n.change; - - n.change = - db.changes().atomicUpdate(n.change.getId(), - new AtomicUpdate() { - @Override - public Change update(Change change) { - change.nextPatchSetId(); - return change; - } - }); - - final PatchSet ps = new PatchSet(n.change.currPatchSetId()); - ps.setCreatedOn(new Timestamp(System.currentTimeMillis())); - ps.setUploader(submitAudit.getAccountId()); - ps.setRevision(new RevId(id.getName())); - insertAncestors(ps.getId(), newCommit); - db.patchSets().insert(Collections.singleton(ps)); - - n.change = - db.changes().atomicUpdate(n.change.getId(), - new AtomicUpdate() { - @Override - public Change update(Change change) { - change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit, - ps.getId())); - return change; - } - }); - - this.submitted.remove(oldChange); - this.submitted.add(n.change); - - if (approvalList != null) { - for (PatchSetApproval a : approvalList) { - db.patchSetApprovals().insert( - Collections.singleton(new PatchSetApproval(ps.getId(), a))); - } - } - - final RefUpdate ru = repo.updateRef(ps.getRefName()); - ru.setExpectedOldObjectId(ObjectId.zeroId()); - ru.setNewObjectId(newCommit); - ru.disableRefLog(); - if (ru.update(rw) != RefUpdate.Result.NEW) { - throw new IOException(String.format( - "Failed to create ref %s in %s: %s", ps.getRefName(), - n.change.getDest().getParentKey().get(), ru.getResult())); - } - replication.fire(n.change.getProject(), ru.getName()); - - newCommit.copyFrom(n); - newCommit.statusCode = CommitMergeStatus.CLEAN_PICK; - commits.put(newCommit.patchsetId.getParentKey(), newCommit); - mergeTip = newCommit; - setRefLogIdent(submitAudit); - } - - private void insertAncestors(PatchSet.Id id, RevCommit src) - throws OrmException { - final int cnt = src.getParentCount(); - List toInsert = new ArrayList(cnt); - for (int p = 0; p < cnt; p++) { - PatchSetAncestor a; - - a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1)); - a.setAncestorRevision(new RevId(src.getParent(p).getId().name())); - toInsert.add(a); - } - db.patchSetAncestors().insert(toInsert); - } - - private ObjectId commit(CommitBuilder mergeCommit) - throws IOException, UnsupportedEncodingException { - ObjectId id = inserter.insert(mergeCommit); - inserter.flush(); - return id; - } - - private boolean contains(List footers, FooterKey key, String val) { - for (final FooterLine line : footers) { - if (line.matches(key) && val.equals(line.getValue())) { - return true; - } - } - return false; - } - - private boolean isSignedOffBy(List footers, String email) { - for (final FooterLine line : footers) { - if (line.matches(FooterKey.SIGNED_OFF_BY) - && email.equals(line.getEmailAddress())) { - return true; - } - } - return false; - } - - private PersonIdent toCommitterIdent(final PatchSetApproval audit) { - if (audit != null) { - return identifiedUserFactory.create(audit.getAccountId()) - .newCommitterIdent(audit.getGranted(), myIdent.getTimeZone()); - } - return myIdent; - } - - private void updateBranch() throws MergeException { if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) { if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) { try { @@ -1060,6 +617,7 @@ public class MergeOp { } } + branchUpdate.setRefLogIdent(refLogIdent); branchUpdate.setForceUpdate(false); branchUpdate.setNewObjectId(mergeTip); branchUpdate.setRefLogMessage("merged", true); @@ -1092,19 +650,9 @@ public class MergeOp { break; case LOCK_FAILURE: - switch (destProject.getSubmitType()) { - case CHERRY_PICK: - case MERGE_ALWAYS: - case MERGE_IF_NECESSARY: - mergeQueue.recheckAfter(destBranch, random.nextInt(1000), MILLISECONDS); - break; - - case FAST_FORWARD_ONLY: - break; // Not mergeable, no need to recheck. - default: - log.warn("Lock failure in project with unknown merge type " - + destProject.getSubmitType()); - break; + if (strategy.retryOnLockFailure()) { + mergeQueue.recheckAfter(destBranch, random.nextInt(1000), + MILLISECONDS); } break; default: @@ -1130,7 +678,7 @@ public class MergeOp { return isMergeable; } - private void updateChangeStatus() { + private void updateChangeStatus(final List submitted) { List merged = new ArrayList(); for (final Change c : submitted) { @@ -1172,10 +720,7 @@ public class MergeOp { } case MISSING_DEPENDENCY: { - final Capable capable = isSubmitStillPossible(commit); - if (capable != Capable.OK) { - sendMergeFail(c, message(c, capable.getMessage()), false); - } + potentiallyStillSubmittable.add(commit); break; } @@ -1188,7 +733,10 @@ public class MergeOp { CreateCodeReviewNotes codeReviewNotes = codeReviewNotesFactory.create(db, repo); try { - codeReviewNotes.create(merged, computeAuthor(merged)); + codeReviewNotes.create( + merged, + computeMergeCommitAuthor(db, identifiedUserFactory, myIdent, rw, + merged)); } catch (CodeReviewNoteCreationException e) { log.error(e.getMessage()); } @@ -1196,7 +744,7 @@ public class MergeOp { GitRepositoryManager.REFS_NOTES_REVIEW); } - private void updateSubscriptions() { + private void updateSubscriptions(final List submitted) { if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) { SubmoduleOp subOp = subOpFactory.create(destBranch, mergeTip, rw, repo, destProject, @@ -1214,32 +762,7 @@ public class MergeOp { private Capable isSubmitStillPossible(final CodeReviewCommit commit) { final Capable capable; final Change c = commit.change; - if (commit.missing == null) { - commit.missing = new ArrayList(); - } - - boolean submitStillPossible = commit.missing.size() > 0; - for (CodeReviewCommit missingCommit : commit.missing) { - loadChangeInfo(missingCommit); - - if (missingCommit.patchsetId == null) { - // The commit doesn't have a patch set, so it cannot be - // submitted to the branch. - // - submitStillPossible = false; - break; - } - - if (!missingCommit.change.currentPatchSetId().equals( - missingCommit.patchsetId)) { - // If the missing commit is not the current patch set, - // the change must be rebased to use the proper parent. - // - submitStillPossible = false; - break; - } - } - + final boolean submitStillPossible = isSubmitForMissingCommitsStillPossible(commit); final long now = System.currentTimeMillis(); final long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY; if (submitStillPossible && now < waitUntil) { @@ -1360,29 +883,6 @@ public class MergeOp { return m; } - private static PatchSetApproval getSubmitter(ReviewDb reviewDb, - PatchSet.Id c) { - if (c == null) { - return null; - } - PatchSetApproval submitter = null; - try { - final List approvals = - reviewDb.patchSetApprovals().byPatchSet(c).toList(); - for (PatchSetApproval a : approvals) { - if (a.getValue() > 0 - && ApprovalCategory.SUBMIT.equals(a.getCategoryId())) { - if (submitter == null - || a.getGranted().compareTo(submitter.getGranted()) > 0) { - submitter = a; - } - } - } - } catch (OrmException e) { - } - return submitter; - } - private void setMerged(final Change c, final ChangeMessage msg) { final Change.Id changeId = c.getId(); // We must pull the patchset out of commits, because the patchset ID is diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java index fe87480b15..3911d96ee1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java @@ -28,14 +28,14 @@ import java.util.Set; class MergeSorter { private final RevWalk rw; - private final RevFlag CAN_MERGE; + private final RevFlag canMergeFlag; private final Set accepted; - MergeSorter(final RevWalk walk, final Set alreadyAccepted, - final RevFlag flagCAN_MERGE) { - rw = walk; - CAN_MERGE = flagCAN_MERGE; - accepted = alreadyAccepted; + MergeSorter(final RevWalk rw, final Set alreadyAccepted, + final RevFlag canMergeFlag) { + this.rw = rw; + this.canMergeFlag = canMergeFlag; + this.accepted = alreadyAccepted; } Collection sort(final Collection incoming) @@ -45,7 +45,7 @@ class MergeSorter { while (!sort.isEmpty()) { final CodeReviewCommit n = removeOne(sort); - rw.resetRetain(CAN_MERGE); + rw.resetRetain(canMergeFlag); rw.markStart(n); for (RevCommit c : accepted) { rw.markUninteresting(c); @@ -54,7 +54,7 @@ class MergeSorter { RevCommit c; final RevCommitList contents = new RevCommitList(); while ((c = rw.next()) != null) { - if (!c.has(CAN_MERGE)) { + if (!c.has(canMergeFlag) || !incoming.contains(c)) { // We cannot merge n as it would bring something we // aren't permitted to merge at this time. Drop n. // diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java new file mode 100644 index 0000000000..2d305b397c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java @@ -0,0 +1,364 @@ +// Copyright (C) 2012 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.git; + +import com.google.gerrit.reviewdb.client.ApprovalCategory; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.PatchSetApproval; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gwtorm.server.OrmException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.merge.ThreeWayMerger; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.sql.Timestamp; +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; +import java.util.TimeZone; + +public class MergeUtil { + private static final Logger log = LoggerFactory.getLogger(MergeUtil.class); + + private static final String R_HEADS_MASTER = + Constants.R_HEADS + Constants.MASTER; + + public static CodeReviewCommit getFirstFastForward( + final CodeReviewCommit mergeTip, final RevWalk rw, + final List toMerge) throws MergeException { + for (final Iterator i = toMerge.iterator(); i.hasNext();) { + try { + final CodeReviewCommit n = i.next(); + if (mergeTip == null || rw.isMergedInto(mergeTip, n)) { + i.remove(); + return n; + } + } catch (IOException e) { + throw new MergeException("Cannot fast-forward test during merge", e); + } + } + return mergeTip; + } + + public static void reduceToMinimalMerge(final MergeSorter mergeSorter, + final List toSort) throws MergeException { + final Collection heads; + try { + heads = mergeSorter.sort(toSort); + } catch (IOException e) { + throw new MergeException("Branch head sorting failed", e); + } + + toSort.clear(); + toSort.addAll(heads); + Collections.sort(toSort, new Comparator() { + @Override + public int compare(final CodeReviewCommit a, final CodeReviewCommit b) { + return a.originalOrder - b.originalOrder; + } + }); + } + + public static PatchSetApproval getSubmitter(final ReviewDb reviewDb, + final PatchSet.Id c) { + if (c == null) { + return null; + } + PatchSetApproval submitter = null; + try { + final List approvals = + reviewDb.patchSetApprovals().byPatchSet(c).toList(); + for (PatchSetApproval a : approvals) { + if (a.getValue() > 0 + && ApprovalCategory.SUBMIT.equals(a.getCategoryId())) { + if (submitter == null + || a.getGranted().compareTo(submitter.getGranted()) > 0) { + submitter = a; + } + } + } + } catch (OrmException e) { + } + return submitter; + } + + public static PersonIdent computeMergeCommitAuthor(final ReviewDb reviewDb, + final IdentifiedUser.GenericFactory identifiedUserFactory, + final PersonIdent myIdent, final RevWalk rw, + final List codeReviewCommits) { + PatchSetApproval submitter = null; + for (final CodeReviewCommit c : codeReviewCommits) { + PatchSetApproval s = getSubmitter(reviewDb, c.patchsetId); + if (submitter == null + || (s != null && s.getGranted().compareTo(submitter.getGranted()) > 0)) { + submitter = s; + } + } + + // Try to use the submitter's identity for the merge commit author. + // If all of the commits being merged are created by the submitter, + // prefer the identity line they used in the commits rather than the + // preferred identity stored in the user account. This way the Git + // commit records are more consistent internally. + // + PersonIdent authorIdent; + if (submitter != null) { + IdentifiedUser who = + identifiedUserFactory.create(submitter.getAccountId()); + Set emails = new HashSet(); + for (RevCommit c : codeReviewCommits) { + try { + rw.parseBody(c); + } catch (IOException e) { + log.warn("Cannot parse commit " + c.name(), e); + continue; + } + emails.add(c.getAuthorIdent().getEmailAddress()); + } + + final Timestamp dt = submitter.getGranted(); + final TimeZone tz = myIdent.getTimeZone(); + if (emails.size() == 1 + && who.getEmailAddresses().contains(emails.iterator().next())) { + authorIdent = + new PersonIdent(codeReviewCommits.get(0).getAuthorIdent(), dt, tz); + } else { + authorIdent = who.newCommitterIdent(dt, tz); + } + } else { + authorIdent = myIdent; + } + return authorIdent; + } + + public static boolean hasMissingDependencies(final MergeSorter mergeSorter, + final CodeReviewCommit toMerge) throws MergeException { + try { + return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge); + } catch (IOException e) { + throw new MergeException("Branch head sorting failed", e); + } + } + + public static CodeReviewCommit mergeOneCommit(final ReviewDb reviewDb, + final IdentifiedUser.GenericFactory identifiedUserFactory, + final PersonIdent myIdent, final Repository repo, final RevWalk rw, + final ObjectInserter inserter, final RevFlag canMergeFlag, + final boolean useContentMerge, final Branch.NameKey destBranch, + final CodeReviewCommit mergeTip, final CodeReviewCommit n) + throws MergeException { + final ThreeWayMerger m = newThreeWayMerger(repo, inserter, useContentMerge); + try { + if (m.merge(new AnyObjectId[] {mergeTip, n})) { + return writeMergeCommit(reviewDb, identifiedUserFactory, myIdent, rw, + inserter, canMergeFlag, destBranch, mergeTip, m.getResultTreeId(), n); + } else { + failed(rw, canMergeFlag, mergeTip, n, CommitMergeStatus.PATH_CONFLICT); + } + } catch (IOException e) { + if (e.getMessage().startsWith("Multiple merge bases for")) { + try { + failed(rw, canMergeFlag, mergeTip, n, + CommitMergeStatus.CRISS_CROSS_MERGE); + } catch (IOException e2) { + throw new MergeException("Cannot merge " + n.name(), e); + } + } else { + throw new MergeException("Cannot merge " + n.name(), e); + } + } + return mergeTip; + } + + private static CodeReviewCommit failed(final RevWalk rw, + final RevFlag canMergeFlag, final CodeReviewCommit mergeTip, + final CodeReviewCommit n, final CommitMergeStatus failure) + throws MissingObjectException, IncorrectObjectTypeException, IOException { + rw.resetRetain(canMergeFlag); + rw.markStart(n); + rw.markUninteresting(mergeTip); + CodeReviewCommit failed; + while ((failed = (CodeReviewCommit) rw.next()) != null) { + failed.statusCode = failure; + } + return failed; + } + + public static CodeReviewCommit writeMergeCommit(final ReviewDb reviewDb, + final IdentifiedUser.GenericFactory identifiedUserFactory, + final PersonIdent myIdent, final RevWalk rw, + final ObjectInserter inserter, final RevFlag canMergeFlag, + final Branch.NameKey destBranch, final CodeReviewCommit mergeTip, + final ObjectId treeId, final CodeReviewCommit n) throws IOException, + MissingObjectException, IncorrectObjectTypeException { + final List merged = new ArrayList(); + rw.resetRetain(canMergeFlag); + rw.markStart(n); + rw.markUninteresting(mergeTip); + for (final RevCommit c : rw) { + final CodeReviewCommit crc = (CodeReviewCommit) c; + if (crc.patchsetId != null) { + merged.add(crc); + } + } + + final StringBuilder msgbuf = new StringBuilder(); + if (merged.size() == 1) { + final CodeReviewCommit c = merged.get(0); + rw.parseBody(c); + msgbuf.append("Merge \""); + msgbuf.append(c.getShortMessage()); + msgbuf.append("\""); + + } else { + msgbuf.append("Merge changes "); + for (final Iterator i = merged.iterator(); i.hasNext();) { + msgbuf.append(i.next().change.getKey().abbreviate()); + if (i.hasNext()) { + msgbuf.append(','); + } + } + } + + if (!R_HEADS_MASTER.equals(destBranch.get())) { + msgbuf.append(" into "); + msgbuf.append(destBranch.getShortName()); + } + + if (merged.size() > 1) { + msgbuf.append("\n\n* changes:\n"); + for (final CodeReviewCommit c : merged) { + rw.parseBody(c); + msgbuf.append(" "); + msgbuf.append(c.getShortMessage()); + msgbuf.append("\n"); + } + } + + PersonIdent authorIdent = + computeMergeCommitAuthor(reviewDb, identifiedUserFactory, myIdent, rw, + merged); + + final CommitBuilder mergeCommit = new CommitBuilder(); + mergeCommit.setTreeId(treeId); + mergeCommit.setParentIds(mergeTip, n); + mergeCommit.setAuthor(authorIdent); + mergeCommit.setCommitter(myIdent); + mergeCommit.setMessage(msgbuf.toString()); + + return (CodeReviewCommit) rw.parseCommit(commit(inserter, mergeCommit)); + } + + public static ThreeWayMerger newThreeWayMerger(final Repository repo, + final ObjectInserter inserter, final boolean useContentMerge) { + ThreeWayMerger m; + if (useContentMerge) { + // Settings for this project allow us to try and + // automatically resolve conflicts within files if needed. + // Use ResolveMerge and instruct to operate in core. + m = MergeStrategy.RESOLVE.newMerger(repo, true); + } else { + // No auto conflict resolving allowed. If any of the + // affected files was modified, merge will fail. + m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo); + } + m.setObjectInserter(new ObjectInserter.Filter() { + @Override + protected ObjectInserter delegate() { + return inserter; + } + + @Override + public void flush() { + } + + @Override + public void release() { + } + }); + return m; + } + + public static ObjectId commit(final ObjectInserter inserter, + final CommitBuilder mergeCommit) throws IOException, + UnsupportedEncodingException { + ObjectId id = inserter.insert(mergeCommit); + inserter.flush(); + return id; + } + + public static PatchSetApproval markCleanMerges(final ReviewDb reviewDb, + final RevWalk rw, final RevFlag canMergeFlag, + final CodeReviewCommit mergeTip, final Set alreadyAccepted) + throws MergeException { + if (mergeTip == null) { + // If mergeTip is null here, branchTip was null, indicating a new branch + // at the start of the merge process. We also elected to merge nothing, + // probably due to missing dependencies. Nothing was cleanly merged. + // + return null; + } + + try { + PatchSetApproval submitApproval = null; + + rw.resetRetain(canMergeFlag); + rw.sort(RevSort.TOPO); + rw.sort(RevSort.REVERSE, true); + rw.markStart(mergeTip); + for (RevCommit c : alreadyAccepted) { + rw.markUninteresting(c); + } + + CodeReviewCommit c; + while ((c = (CodeReviewCommit) rw.next()) != null) { + if (c.patchsetId != null) { + c.statusCode = CommitMergeStatus.CLEAN_MERGE; + if (submitApproval == null) { + submitApproval = getSubmitter(reviewDb, c.patchsetId); + } + } + } + + return submitApproval; + } catch (IOException e) { + throw new MergeException("Cannot mark clean merges", e); + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java new file mode 100644 index 0000000000..b6770a9ee1 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java @@ -0,0 +1,171 @@ +// Copyright (C) 2012 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.git; + +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSetApproval; +import com.google.gerrit.reviewdb.client.Project.SubmitType; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.IdentifiedUser; + +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevWalk; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Base class that submit strategies must extend. A submit strategy for a + * certain {@link SubmitType} defines how the submitted commits should be + * merged. + */ +public abstract class SubmitStrategy { + + private PersonIdent refLogIdent; + + static class Arguments { + protected final IdentifiedUser.GenericFactory identifiedUserFactory; + protected final PersonIdent myIdent; + protected final ReviewDb db; + + protected final Repository repo; + protected final RevWalk rw; + protected final ObjectInserter inserter; + protected final RevFlag canMergeFlag; + protected final Set alreadyAccepted; + protected final Branch.NameKey destBranch; + protected final boolean useContentMerge; + protected final MergeSorter mergeSorter; + + Arguments(final IdentifiedUser.GenericFactory identifiedUserFactory, + final PersonIdent myIdent, final ReviewDb db, final Repository repo, + final RevWalk rw, final ObjectInserter inserter, + final RevFlag canMergeFlag, final Set alreadyAccepted, + final Branch.NameKey destBranch, final boolean useContentMerge) { + this.identifiedUserFactory = identifiedUserFactory; + this.myIdent = myIdent; + this.db = db; + + this.repo = repo; + this.rw = rw; + this.inserter = inserter; + this.canMergeFlag = canMergeFlag; + this.alreadyAccepted = alreadyAccepted; + this.destBranch = destBranch; + this.useContentMerge = useContentMerge; + this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag); + } + } + + protected final Arguments args; + + SubmitStrategy(final Arguments args) { + this.args = args; + } + + /** + * Runs this submit strategy. If possible the provided commits will be merged + * with this submit strategy. + * + * @param mergeTip the mergeTip + * @param toMerge the list of submitted commits that should be merged using + * this submit strategy + * @return the new mergeTip + * @throws MergeException + */ + public final CodeReviewCommit run(final CodeReviewCommit mergeTip, + final List toMerge) throws MergeException { + refLogIdent = null; + return _run(mergeTip, toMerge); + } + + /** + * Runs this submit strategy. If possible the provided commits will be merged + * with this submit strategy. + * + * @param mergeTip the mergeTip + * @param toMerge the list of submitted commits that should be merged using + * this submit strategy + * @return the new mergeTip + * @throws MergeException + */ + protected abstract CodeReviewCommit _run(CodeReviewCommit mergeTip, + List toMerge) throws MergeException; + + /** + * Returns the PersonIdent that should be used for the ref log entries when + * updating the destination branch. The ref log identity may be set after the + * {@link #run(CodeReviewCommit, List)} method finished. + * + * Do only call this method after the {@link #run(CodeReviewCommit, List)} + * method has been invoked. + * + * @return the ref log identity, may be null + */ + public final PersonIdent getRefLogIdent() { + return refLogIdent; + } + + /** + * Returns all commits that have been newly created for the changes that are + * getting merged. + * + * By default this method is returning an empty map, but subclasses may + * overwrite this method to provide newly created commits. + * + * Do only call this method after the {@link #run(CodeReviewCommit, List)} + * method has been invoked. + * + * @return new commits created for changes that are getting merged + */ + public Map getNewCommits() { + return Collections.emptyMap(); + } + + /** + * Returns whether a merge that failed with + * {@link RefUpdate.Result#LOCK_FAILURE} should be retried. + * + * May be overwritten by subclasses. + * + * @return true if a merge that failed with + * {@link RefUpdate.Result#LOCK_FAILURE} should be retried, otherwise + * false + */ + public boolean retryOnLockFailure() { + return true; + } + + /** + * Sets the ref log identity if it wasn't set yet. + * + * @param submitApproval the approval that submitted the patch set + */ + protected final void setRefLogIdent(final PatchSetApproval submitApproval) { + if (refLogIdent == null && submitApproval != null) { + refLogIdent = + args.identifiedUserFactory.create(submitApproval.getAccountId()) + .newRefLogIdent(); + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java new file mode 100644 index 0000000000..e942f71252 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java @@ -0,0 +1,94 @@ +// Copyright (C) 2012 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.git; + +import com.google.gerrit.common.data.ApprovalTypes; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Project.SubmitType; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +import javax.annotation.Nullable; + +/** Factory to create a {@link SubmitStrategy} for a {@link SubmitType}. */ +public class SubmitStrategyFactory { + private static final Logger log = LoggerFactory + .getLogger(SubmitStrategyFactory.class); + + private final IdentifiedUser.GenericFactory identifiedUserFactory; + private final PersonIdent myIdent; + private final PatchSetInfoFactory patchSetInfoFactory; + private final Provider urlProvider; + private final ApprovalTypes approvalTypes; + private final GitReferenceUpdated replication; + + @Inject + SubmitStrategyFactory( + final IdentifiedUser.GenericFactory identifiedUserFactory, + @GerritPersonIdent final PersonIdent myIdent, + final PatchSetInfoFactory patchSetInfoFactory, + @CanonicalWebUrl @Nullable final Provider urlProvider, + final ApprovalTypes approvalTypes, final GitReferenceUpdated replication) { + this.identifiedUserFactory = identifiedUserFactory; + this.myIdent = myIdent; + this.patchSetInfoFactory = patchSetInfoFactory; + this.urlProvider = urlProvider; + this.approvalTypes = approvalTypes; + this.replication = replication; + } + + public SubmitStrategy create(final SubmitType submitType, final ReviewDb db, + final Repository repo, final RevWalk rw, final ObjectInserter inserter, + final RevFlag canMergeFlag, final Set alreadyAccepted, + final Branch.NameKey destBranch, final boolean useContentMerge) + throws MergeException { + final SubmitStrategy.Arguments args = + new SubmitStrategy.Arguments(identifiedUserFactory, myIdent, db, repo, + rw, inserter, canMergeFlag, alreadyAccepted, destBranch, + useContentMerge); + switch (submitType) { + case CHERRY_PICK: + return new CherryPick(args, patchSetInfoFactory, urlProvider, + approvalTypes, replication); + case FAST_FORWARD_ONLY: + return new FastForwardOnly(args); + case MERGE_ALWAYS: + return new MergeAlways(args); + case MERGE_IF_NECESSARY: + return new MergeIfNecessary(args); + default: + final String errorMsg = "No submit strategy for: " + submitType; + log.error(errorMsg); + throw new MergeException(errorMsg); + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java index 033faaae1e..4494853c76 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java @@ -16,14 +16,13 @@ package com.google.gerrit.server.project; import com.google.gerrit.common.data.PermissionRange; import com.google.gerrit.common.data.SubmitRecord; +import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.rules.PrologEnvironment; -import com.google.gerrit.rules.StoredValues; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.query.change.ChangeData; @@ -32,23 +31,18 @@ import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.util.Providers; -import com.googlecode.prolog_cafe.compiler.CompileException; import com.googlecode.prolog_cafe.lang.IntegerTerm; import com.googlecode.prolog_cafe.lang.ListTerm; import com.googlecode.prolog_cafe.lang.Prolog; -import com.googlecode.prolog_cafe.lang.PrologException; import com.googlecode.prolog_cafe.lang.StructureTerm; import com.googlecode.prolog_cafe.lang.Term; -import com.googlecode.prolog_cafe.lang.VariableTerm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.annotation.Nullable; @@ -303,6 +297,7 @@ public class ChangeControl { return canSubmit(db, patchSet, null, false, false); } + @SuppressWarnings("unchecked") public List canSubmit(ReviewDb db, PatchSet patchSet, @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed) { if (!allowClosed && change.getStatus().isClosed()) { @@ -334,103 +329,18 @@ public class ChangeControl { return logRuleError("Cannot read patch set " + patchSet.getId(), err); } - List results = new ArrayList(); - Term submitRule; - ProjectState projectState = getProjectControl().getProjectState(); - PrologEnvironment env; - + List results; + SubmitRuleEvaluator evaluator; try { - env = projectState.newPrologEnvironment(); - } catch (CompileException err) { - return logRuleError("Cannot consult rules.pl for " - + getProject().getName(), err); - } - - try { - env.set(StoredValues.REVIEW_DB, db); - env.set(StoredValues.CHANGE, change); - env.set(StoredValues.CHANGE_DATA, cd); - env.set(StoredValues.PATCH_SET, patchSet); - env.set(StoredValues.CHANGE_CONTROL, this); - - submitRule = env.once( - "gerrit", "locate_submit_rule", - new VariableTerm()); - if (submitRule == null) { - return logRuleError("No user:submit_rule found for " - + getProject().getName()); - } - - if (fastEvalLabels) { - env.once("gerrit", "assume_range_from_label"); - } - - try { - for (Term[] template : env.all( - "gerrit", "can_submit", - submitRule, - new VariableTerm())) { - results.add(template[1]); - } - } catch (PrologException err) { - return logRuleError("Exception calling " + submitRule + " on change " - + change.getId() + " of " + getProject().getName(), err); - } catch (RuntimeException err) { - return logRuleError("Exception calling " + submitRule + " on change " - + change.getId() + " of " + getProject().getName(), err); - } - - ProjectState parentState = projectState.getParentState(); - PrologEnvironment childEnv = env; - Set projectsSeen = new HashSet(); - projectsSeen.add(getProject().getNameKey()); - - while (parentState != null) { - if (!projectsSeen.add(parentState.getProject().getNameKey())) { - //parent has been seen before, stop walk up inheritance tree - break; - } - PrologEnvironment parentEnv; - try { - parentEnv = parentState.newPrologEnvironment(); - } catch (CompileException err) { - return logRuleError("Cannot consult rules.pl for " - + parentState.getProject().getName(), err); - } - - parentEnv.copyStoredValues(childEnv); - Term filterRule = - parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm()); - if (filterRule != null) { - try { - if (fastEvalLabels) { - env.once("gerrit", "assume_range_from_label"); - } - - Term resultsTerm = toListTerm(results); - results.clear(); - Term[] template = parentEnv.once( - "gerrit", "filter_submit_results", - filterRule, - resultsTerm, - new VariableTerm()); - @SuppressWarnings("unchecked") - final List termList = ((ListTerm) template[2]).toJava(); - results.addAll(termList); - } catch (PrologException err) { - return logRuleError("Exception calling " + filterRule + " on change " - + change.getId() + " of " + parentState.getProject().getName(), err); - } catch (RuntimeException err) { - return logRuleError("Exception calling " + filterRule + " on change " - + change.getId() + " of " + parentState.getProject().getName(), err); - } - } - - parentState = parentState.getParentState(); - childEnv = parentEnv; - } - } finally { - env.close(); + evaluator = new SubmitRuleEvaluator(db, patchSet, + getProjectControl(), + this, change, cd, + fastEvalLabels, + "locate_submit_rule", "can_submit", + "locate_submit_filter", "filter_submit_results"); + results = evaluator.evaluate().toJava(); + } catch (RuleEvalException e) { + return logRuleError(e.getMessage(), e); } if (results.isEmpty()) { @@ -438,12 +348,13 @@ public class ChangeControl { // at least one result informing the caller of the labels that are // required for this change to be submittable. Each label will indicate // whether or not that is actually possible given the permissions. - log.error("Submit rule " + submitRule + " for change " + change.getId() - + " of " + getProject().getName() + " has no solution."); + log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change " + + change.getId() + " of " + getProject().getName() + + " has no solution."); return ruleError("Project submit rule has no solution"); } - return resultsToSubmitRecord(submitRule, results); + return resultsToSubmitRecord(evaluator.getSubmitRule(), results); } /** @@ -528,6 +439,65 @@ public class ChangeControl { return out; } + public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet) { + return getSubmitTypeRecord(db, patchSet, null); + } + + @SuppressWarnings("unchecked") + public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet, + @Nullable ChangeData cd) { + if (!patchSet.getId().equals(change.currentPatchSetId())) { + return typeRuleError("Patch set " + patchSet.getPatchSetId() + + " is not current"); + } + + try { + if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db, cd)) { + return typeRuleError("Patch set " + patchSet.getPatchSetId() + + " not found"); + } + if (patchSet.isDraft() && !isDraftVisible(db, cd)) { + return typeRuleError("Patch set " + patchSet.getPatchSetId() + + " not found"); + } + } catch (OrmException err) { + return logTypeRuleError("Cannot read patch set " + patchSet.getId(), + err); + } + + List results; + SubmitRuleEvaluator evaluator; + try { + evaluator = new SubmitRuleEvaluator(db, patchSet, + getProjectControl(), this, change, cd, + false, + "locate_submit_type", "get_submit_type", + "locate_submit_type_filter", "filter_submit_type_results"); + results = evaluator.evaluate().toJava(); + } catch (RuleEvalException e) { + return logTypeRuleError(e.getMessage(), e); + } + + if (results.isEmpty()) { + // Should never occur for a well written rule + log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change " + + change.getId() + " of " + getProject().getName() + + " has no solution."); + return typeRuleError("Project submit rule has no solution"); + } + + // Take only the first result and convert it to SubmitTypeRecord + // This logic will need to change once we support multiple submit types + // in the UI + String typeName = results.get(0); + try { + return SubmitTypeRecord.OK( + Project.SubmitType.valueOf(typeName.toUpperCase())); + } catch (IllegalArgumentException e) { + return logInvalidType(evaluator.getSubmitRule(), typeName); + } + } + private List logInvalidResult(Term rule, Term record) { return logRuleError("Submit rule " + rule + " for change " + change.getId() + " of " + getProject().getName() + " output invalid result: " + record); @@ -550,6 +520,29 @@ public class ChangeControl { return Collections.singletonList(rec); } + private SubmitTypeRecord logInvalidType(Term rule, String record) { + return logTypeRuleError("Submit type rule " + rule + " for change " + + change.getId() + " of " + getProject().getName() + + " output invalid result: " + record); + } + + private SubmitTypeRecord logTypeRuleError(String err, Exception e) { + log.error(err, e); + return typeRuleError("Error evaluating project type rules, check server log"); + } + + private SubmitTypeRecord logTypeRuleError(String err) { + log.error(err); + return typeRuleError("Error evaluating project type rules, check server log"); + } + + private SubmitTypeRecord typeRuleError(String err) { + SubmitTypeRecord rec = new SubmitTypeRecord(); + rec.status = SubmitTypeRecord.Status.RULE_ERROR; + rec.errorMessage = err; + return rec; + } + private void appliedBy(SubmitRecord.Label label, Term status) { if (status.isStructure() && status.arity() == 1) { Term who = status.arg(0); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RuleEvalException.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RuleEvalException.java new file mode 100644 index 0000000000..9dae11c4d6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RuleEvalException.java @@ -0,0 +1,26 @@ +// Copyright (C) 2012 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.project; + +@SuppressWarnings("serial") +public class RuleEvalException extends Exception { + public RuleEvalException(String message) { + super(message); + } + + RuleEvalException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java new file mode 100644 index 0000000000..197762469e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java @@ -0,0 +1,207 @@ +// Copyright (C) 2012 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.project; + +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.rules.PrologEnvironment; +import com.google.gerrit.rules.StoredValues; +import com.google.gerrit.server.query.change.ChangeData; + +import com.googlecode.prolog_cafe.compiler.CompileException; +import com.googlecode.prolog_cafe.lang.ListTerm; +import com.googlecode.prolog_cafe.lang.Prolog; +import com.googlecode.prolog_cafe.lang.PrologException; +import com.googlecode.prolog_cafe.lang.Term; +import com.googlecode.prolog_cafe.lang.VariableTerm; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.Nullable; + +/** + * Evaluates a submit-like Prolog rule found in the rules.pl file of the current + * project and filters the results through rules found in the parent projects, + * all the way up to All-Projects. + */ +public class SubmitRuleEvaluator { + private final ReviewDb db; + private final PatchSet patchSet; + private final ProjectControl projectControl; + private final ChangeControl changeControl; + private final Change change; + private final ChangeData cd; + private final boolean fastEvalLabels; + private final String userRuleLocatorName; + private final String userRuleWrapperName; + private final String filterRuleLocatorName; + private final String filterRuleWrapperName; + + private Term submitRule; + private String projectName; + + /** + * @param userRuleLocatorName The name of the rule used to locate the + * user-supplied rule. + * @param userRuleWrapperName The name of the wrapper rule used to evaluate + * the user-supplied rule. + * @param filterRuleLocatorName The name of the rule used to locate the filter + * rule. + * @param filterRuleWrapperName The name of the rule used to evaluate the + * filter rule. + */ + SubmitRuleEvaluator(ReviewDb db, PatchSet patchSet, + ProjectControl projectControl, + ChangeControl changeControl, Change change, @Nullable ChangeData cd, + boolean fastEvalLabels, + String userRuleLocatorName, String userRuleWrapperName, + String filterRuleLocatorName, String filterRuleWrapperName) { + this.db = db; + this.patchSet = patchSet; + this.projectControl = projectControl; + this.changeControl = changeControl; + this.change = change; + this.cd = cd; + this.fastEvalLabels = fastEvalLabels; + this.userRuleLocatorName = userRuleLocatorName; + this.userRuleWrapperName = userRuleWrapperName; + this.filterRuleLocatorName = filterRuleLocatorName; + this.filterRuleWrapperName = filterRuleWrapperName; + } + + /** + * Evaluates the given rule and filters. + * + * Sets the {@link #submitRule} to the Term found by the + * {@link #userRuleLocatorName}. This can be used when reporting error(s) on + * unexpected return value of this method. + * + * @return List of {@link Term} objects returned from the evaluated rules. + * @throws RuleEvalException + */ + ListTerm evaluate() throws RuleEvalException { + List results = new ArrayList(); + ProjectState projectState = projectControl.getProjectState(); + PrologEnvironment env; + + try { + env = projectState.newPrologEnvironment(); + } catch (CompileException err) { + throw new RuleEvalException("Cannot consult rules.pl for " + + getProjectName(), err); + } + + try { + env.set(StoredValues.REVIEW_DB, db); + env.set(StoredValues.CHANGE, change); + env.set(StoredValues.CHANGE_DATA, cd); + env.set(StoredValues.PATCH_SET, patchSet); + env.set(StoredValues.CHANGE_CONTROL, changeControl); + + submitRule = env.once("gerrit", userRuleLocatorName, new VariableTerm()); + if (fastEvalLabels) { + env.once("gerrit", "assume_range_from_label"); + } + + try { + for (Term[] template : env.all("gerrit", userRuleWrapperName, + submitRule, new VariableTerm())) { + results.add(template[1]); + } + } catch (PrologException err) { + throw new RuleEvalException("Exception calling " + submitRule + + " on change " + change.getId() + " of " + getProjectName(), + err); + } catch (RuntimeException err) { + throw new RuleEvalException("Exception calling " + submitRule + + " on change " + change.getId() + " of " + getProjectName(), + err); + } + + ProjectState parentState = projectState.getParentState(); + PrologEnvironment childEnv = env; + Set projectsSeen = new HashSet(); + projectsSeen.add(projectState.getProject().getNameKey()); + + Term resultsTerm = toListTerm(results); + while (parentState != null) { + if (!projectsSeen.add(parentState.getProject().getNameKey())) { + // parent has been seen before, stop walk up inheritance tree + break; + } + PrologEnvironment parentEnv; + try { + parentEnv = parentState.newPrologEnvironment(); + } catch (CompileException err) { + throw new RuleEvalException("Cannot consult rules.pl for " + + parentState.getProject().getName(), err); + } + + parentEnv.copyStoredValues(childEnv); + Term filterRule = + parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm()); + try { + if (fastEvalLabels) { + env.once("gerrit", "assume_range_from_label"); + } + + Term[] template = + parentEnv.once("gerrit", filterRuleWrapperName, filterRule, + resultsTerm, new VariableTerm()); + resultsTerm = template[2]; + } catch (PrologException err) { + throw new RuleEvalException("Exception calling " + filterRule + + " on change " + change.getId() + " of " + + parentState.getProject().getName(), err); + } catch (RuntimeException err) { + throw new RuleEvalException("Exception calling " + filterRule + + " on change " + change.getId() + " of " + + parentState.getProject().getName(), err); + } + + parentState = parentState.getParentState(); + childEnv = parentEnv; + } + + return (ListTerm) resultsTerm; + } finally { + env.close(); + } + } + + private static Term toListTerm(List terms) { + Term list = Prolog.Nil; + for (int i = terms.size() - 1; i >= 0; i--) { + list = new ListTerm(terms.get(i), list); + } + return list; + } + + Term getSubmitRule() { + return submitRule; + } + + private String getProjectName() { + if (projectName == null) { + projectName = projectControl.getProjectState().getProject().getName(); + } + return projectName; + } +} diff --git a/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java new file mode 100644 index 0000000000..0f173c7d69 --- /dev/null +++ b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java @@ -0,0 +1,58 @@ +// Copyright (C) 2012 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 gerrit; + +import com.google.gerrit.reviewdb.client.Project.SubmitType; +import com.google.gerrit.rules.StoredValues; +import com.google.gerrit.server.project.ChangeControl; + +import com.googlecode.prolog_cafe.lang.Operation; +import com.googlecode.prolog_cafe.lang.Predicate; +import com.googlecode.prolog_cafe.lang.Prolog; +import com.googlecode.prolog_cafe.lang.PrologException; +import com.googlecode.prolog_cafe.lang.SymbolTerm; +import com.googlecode.prolog_cafe.lang.Term; + +public class PRED_project_default_submit_type_1 extends Predicate.P1 { + + private static final SymbolTerm[] term; + + static { + SubmitType[] val = SubmitType.values(); + term = new SymbolTerm[val.length]; + for (int i = 0; i < val.length; i++) { + term[i] = SymbolTerm.create(val[i].name()); + } + } + + public PRED_project_default_submit_type_1(Term a1, Operation n) { + arg1 = a1; + cont = n; + } + + @Override + public Operation exec(Prolog engine) throws PrologException { + engine.setB0(); + Term a1 = arg1.dereference(); + + ChangeControl control = StoredValues.CHANGE_CONTROL.get(engine); + SubmitType submitType = control.getProject().getSubmitType(); + + if (!a1.unify(term[submitType.ordinal()], engine.trail)) { + return engine.fail(); + } + return cont; + } +} diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl index a75acc0199..59289c0a87 100644 --- a/gerrit-server/src/main/prolog/gerrit_common.pl +++ b/gerrit-server/src/main/prolog/gerrit_common.pl @@ -140,12 +140,12 @@ not_same(_, _). :- public can_submit/2. %% can_submit(SubmitRule, S) :- - call_submit_rule(SubmitRule, Tmp), + call_rule(SubmitRule, Tmp), Tmp =.. [submit | Ls], ( is_all_ok(Ls) -> S = ok(Tmp), ! ; S = not_ready(Tmp) ). -call_submit_rule(P:X, Arg) :- !, F =.. [X, Arg], P:F. -call_submit_rule(X, Arg) :- !, F =.. [X, Arg], F. +call_rule(P:X, Arg) :- !, F =.. [X, Arg], P:F. +call_rule(X, Arg) :- !, F =.. [X, Arg], F. is_all_ok([]). is_all_ok([label(_, ok(__)) | Ls]) :- is_all_ok(Ls). @@ -153,6 +153,21 @@ is_all_ok([label(_, may(__)) | Ls]) :- is_all_ok(Ls). is_all_ok(_) :- fail. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% locate_helper +%% +%% Returns user:Func if it exists otherwise returns gerrit:Default + +locate_helper(Func, Default, Arity, user:Func) :- + '$compiled_predicate'(user, Func, Arity), !. +locate_helper(Func, Default, Arity, user:Func) :- + listN(Arity, P), C =.. [Func | P], clause(user:C, _), !. +locate_helper(Func, Default, _, gerrit:Default). + +listN(0, []). +listN(N, [_|T]) :- N > 0, N1 is N - 1, listN(N1, T). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% locate_submit_rule/1: @@ -164,17 +179,32 @@ is_all_ok(_) :- fail. %% locate_submit_rule(RuleName) :- - '$compiled_predicate'(user, submit_rule, 1), - !, - RuleName = user:submit_rule - . -locate_submit_rule(RuleName) :- - clause(user:submit_rule(_), _), - !, - RuleName = user:submit_rule - . -locate_submit_rule(RuleName) :- - RuleName = gerrit:default_submit. + locate_helper(submit_rule, default_submit, 1, RuleName). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% get_submit_type/2: +%% +%% Executes the SubmitTypeRule and return the first solution +%% +:- public get_submit_type/2. +%% +get_submit_type(SubmitTypeRule, A) :- + call_rule(SubmitTypeRule, A), !. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% locate_submit_type/1: +%% +%% Finds a submit_type_rule depending on what rules are available. +%% If none are available, use project_default_submit_type/1. +%% +:- public locate_submit_type/1. +%% +locate_submit_type(RuleName) :- + locate_helper(submit_type, project_default_submit_type, 1, RuleName). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -306,6 +336,17 @@ filter_submit_results(Filter, [], Out, Out). call_submit_filter(P:X, R, S) :- !, F =.. [X, R, S], P:F. call_submit_filter(X, R, S) :- F =.. [X, R, S], F. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% filter_submit_type_results/3: +%% +%% Executes the submit_type_filter against the result, +%% returns the filtered result. +%% +:- public filter_submit_type_results/3. +%% +filter_submit_type_results(Filter, In, Out) :- call_submit_filter(Filter, In, Out). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -316,15 +357,26 @@ call_submit_filter(X, R, S) :- F =.. [X, R, S], F. :- public locate_submit_filter/1. %% locate_submit_filter(FilterName) :- - '$compiled_predicate'(user, submit_filter, 2), - !, - FilterName = user:submit_filter - . -locate_submit_filter(FilterName) :- - clause(user:submit_filter(_,_), _), - FilterName = user:submit_filter - . + locate_helper(submit_filter, noop_filter, 2, FilterName). +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% noop_filter/2: +%% +:- public noop_filter/2. +%% +noop_filter(In, In). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% locate_submit_type_filter/1: +%% +%% Finds a submit_type_filter if available. +%% +:- public locate_submit_type_filter/1. +%% +locate_submit_type_filter(FilterName) :- + locate_helper(submit_type_filter, noop_filter, 2, FilterName). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%