Merge feature 'Control submit type from Prolog'

* Control submit type from Prolog:
  Support controlling the submit type for changes from Prolog
  Remove unnecessary Prolog-to-Java conversion from submit filters
  Use locate_helper to replace locate_* predicates
  ChangeControl: Extract Prolog submit rule evaluation to its own method
  Display submit type in the change info block
  Support changes with different submit types in MergeOp
  Make submit strategies re-executeable
  Refactor MergeOp: reduce number of member variables, make names consistent
  Refactor MergeOp: implement each submit strategy in an own class
  Refactor MergeOp: extract utility methods into MergeUtil class

Change-Id: I71d4f5757ab96b5eb8456972398a7bb9fa2a7d4c
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2012-10-15 13:46:38 +02:00
26 changed files with 1946 additions and 841 deletions

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.common.data;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -40,6 +41,8 @@ public class ChangeDetail {
protected List<PatchSet> patchSets; protected List<PatchSet> patchSets;
protected List<ApprovalDetail> approvals; protected List<ApprovalDetail> approvals;
protected List<SubmitRecord> submitRecords; protected List<SubmitRecord> submitRecords;
protected Project.SubmitType submitType;
protected SubmitTypeRecord submitTypeRecord;
protected boolean canSubmit; protected boolean canSubmit;
protected List<ChangeMessage> messages; protected List<ChangeMessage> messages;
protected PatchSet.Id currentPatchSetId; protected PatchSet.Id currentPatchSetId;
@@ -187,6 +190,14 @@ public class ChangeDetail {
return submitRecords; return submitRecords;
} }
public void setSubmitTypeRecord(SubmitTypeRecord submitTypeRecord) {
this.submitTypeRecord = submitTypeRecord;
}
public SubmitTypeRecord getSubmitTypeRecord() {
return submitTypeRecord;
}
public boolean isCurrentPatchSet(final PatchSetDetail detail) { public boolean isCurrentPatchSet(final PatchSetDetail detail) {
return currentPatchSetId != null return currentPatchSetId != null
&& detail.getPatchSet().getId().equals(currentPatchSetId); && detail.getPatchSet().getId().equals(currentPatchSetId);

View File

@@ -19,6 +19,7 @@ import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchLineComment; import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo; import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.Project;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -33,6 +34,7 @@ public class PatchSetPublishDetail {
protected List<PermissionRange> labels; protected List<PermissionRange> labels;
protected List<ApprovalDetail> approvals; protected List<ApprovalDetail> approvals;
protected List<SubmitRecord> submitRecords; protected List<SubmitRecord> submitRecords;
protected SubmitTypeRecord submitTypeRecord;
protected List<PatchSetApproval> given; protected List<PatchSetApproval> given;
protected boolean canSubmit; protected boolean canSubmit;
@@ -61,6 +63,14 @@ public class PatchSetPublishDetail {
return submitRecords; return submitRecords;
} }
public void setSubmitTypeRecord(SubmitTypeRecord submitTypeRecord) {
this.submitTypeRecord = submitTypeRecord;
}
public SubmitTypeRecord getSubmitTypeRecord() {
return submitTypeRecord;
}
public List<PatchSetApproval> getGiven() { public List<PatchSetApproval> getGiven() {
return given; return given;
} }

View File

@@ -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.
* <p>
* 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();
}
}

View File

@@ -95,6 +95,7 @@ public interface ChangeConstants extends Constants {
String changeInfoBlockUploaded(); String changeInfoBlockUploaded();
String changeInfoBlockUpdated(); String changeInfoBlockUpdated();
String changeInfoBlockStatus(); String changeInfoBlockStatus();
String changeInfoBlockSubmitType();
String changePermalink(); String changePermalink();
String changeInfoBlockCanMerge(); String changeInfoBlockCanMerge();
String changeInfoBlockCanMergeYes(); String changeInfoBlockCanMergeYes();

View File

@@ -72,6 +72,7 @@ changeInfoBlockTopic = Topic
changeInfoBlockUploaded = Uploaded changeInfoBlockUploaded = Uploaded
changeInfoBlockUpdated = Updated changeInfoBlockUpdated = Updated
changeInfoBlockStatus = Status changeInfoBlockStatus = Status
changeInfoBlockSubmitType = Submit Type
changePermalink = Permalink changePermalink = Permalink
changeInfoBlockCanMerge = Can Merge changeInfoBlockCanMerge = Can Merge
changeInfoBlockCanMergeYes = Yes changeInfoBlockCanMergeYes = Yes

View File

@@ -15,8 +15,10 @@
package com.google.gerrit.client.changes; package com.google.gerrit.client.changes;
import com.google.gerrit.common.data.AccountInfoCache; 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.Change;
import com.google.gerrit.reviewdb.client.PatchSetInfo; 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.Composite;
import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwtexpui.globalkey.client.KeyCommandSet; 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, public void display(Change chg, Boolean starred, PatchSetInfo info,
final AccountInfoCache acc) { final AccountInfoCache acc, SubmitTypeRecord submitTypeRecord) {
infoBlock.display(chg, acc); infoBlock.display(chg, acc, submitTypeRecord);
messageBlock.display(chg.getId(), starred, info.getMessage()); messageBlock.display(chg.getId(), starred, info.getMessage());
} }
} }

View File

@@ -21,6 +21,7 @@ import com.google.gerrit.client.ui.AccountLink;
import com.google.gerrit.client.ui.BranchLink; import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.ProjectLink; import com.google.gerrit.client.ui.ProjectLink;
import com.google.gerrit.common.data.AccountInfoCache; 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.Branch;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.user.client.ui.Composite; 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_TOPIC = 4;
private static final int R_UPLOADED = 5; private static final int R_UPLOADED = 5;
private static final int R_UPDATED = 6; private static final int R_UPDATED = 6;
private static final int R_STATUS = 7; private static final int R_SUBMIT_TYPE = 7;
private static final int R_MERGE_TEST = 8; private static final int R_STATUS = 8;
private static final int R_CNT = 9; private static final int R_MERGE_TEST = 9;
private static final int R_CNT = 10;
private final Grid table; private final Grid table;
@@ -59,6 +61,7 @@ public class ChangeInfoBlock extends Composite {
initRow(R_UPLOADED, Util.C.changeInfoBlockUploaded()); initRow(R_UPLOADED, Util.C.changeInfoBlockUploaded());
initRow(R_UPDATED, Util.C.changeInfoBlockUpdated()); initRow(R_UPDATED, Util.C.changeInfoBlockUpdated());
initRow(R_STATUS, Util.C.changeInfoBlockStatus()); initRow(R_STATUS, Util.C.changeInfoBlockStatus());
initRow(R_SUBMIT_TYPE, Util.C.changeInfoBlockSubmitType());
if (Gerrit.getConfig().testChangeMerge()) { if (Gerrit.getConfig().testChangeMerge()) {
initRow(R_MERGE_TEST, Util.C.changeInfoBlockCanMerge()); 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()); 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(); final Branch.NameKey dst = chg.getDest();
CopyableLabel changeIdLabel = CopyableLabel changeIdLabel =
@@ -94,6 +98,14 @@ public class ChangeInfoBlock extends Composite {
table.setText(R_UPLOADED, 1, mediumFormat(chg.getCreatedOn())); table.setText(R_UPLOADED, 1, mediumFormat(chg.getCreatedOn()));
table.setText(R_UPDATED, 1, mediumFormat(chg.getLastUpdatedOn())); table.setText(R_UPDATED, 1, mediumFormat(chg.getLastUpdatedOn()));
table.setText(R_STATUS, 1, Util.toLongString(chg.getStatus())); 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(); final Change.Status status = chg.getStatus();
if (Gerrit.getConfig().testChangeMerge()) { if (Gerrit.getConfig().testChangeMerge()) {
if (status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) { if (status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) {

View File

@@ -282,7 +282,7 @@ public class ChangeScreen extends Screen
descriptionBlock.display(detail.getChange(), descriptionBlock.display(detail.getChange(),
detail.isStarred(), detail.isStarred(),
detail.getCurrentPatchSetDetail().getInfo(), detail.getCurrentPatchSetDetail().getInfo(),
detail.getAccounts()); detail.getAccounts(), detail.getSubmitTypeRecord());
dependsOn.display(detail.getDependsOn()); dependsOn.display(detail.getDependsOn());
neededBy.display(detail.getNeededBy()); neededBy.display(detail.getNeededBy());
approvals.display(detail); approvals.display(detail);

View File

@@ -274,7 +274,8 @@ public class PublishCommentScreen extends AccountScreen implements
private void display(final PatchSetPublishDetail r) { private void display(final PatchSetPublishDetail r) {
setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(), setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
patchSetId.get())); 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()) { if (r.getChange().getStatus().isOpen()) {
initApprovals(r, approvalPanel); initApprovals(r, approvalPanel);

View File

@@ -168,6 +168,8 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
} }
detail.setSubmitRecords(submitRecords); detail.setSubmitRecords(submitRecords);
detail.setSubmitTypeRecord(control.getSubmitTypeRecord(db, patch));
patchsetsById = new HashMap<PatchSet.Id, PatchSet>(); patchsetsById = new HashMap<PatchSet.Id, PatchSet>();
loadPatchSets(); loadPatchSets();
loadMessages(); loadMessages();

View File

@@ -186,6 +186,8 @@ final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail>
detail.setSubmitRecords(submitRecords); detail.setSubmitRecords(submitRecords);
} }
detail.setSubmitTypeRecord(control.getSubmitTypeRecord(db, patchSet));
detail.setLabels(allowed); detail.setLabels(allowed);
detail.setGiven(given); detail.setGiven(given);
loadApprovals(detail, control); loadApprovals(detail, control);

View File

@@ -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<String> urlProvider;
private final ApprovalTypes approvalTypes;
private final GitReferenceUpdated replication;
private final Map<Change.Id, CodeReviewCommit> newCommits;
CherryPick(final SubmitStrategy.Arguments args,
final PatchSetInfoFactory patchSetInfoFactory,
final Provider<String> 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<Change.Id, CodeReviewCommit>();
}
@Override
protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
final List<CodeReviewCommit> 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<Change.Id, CodeReviewCommit> getNewCommits() {
return newCommits;
}
private CodeReviewCommit writeCherryPickCommit(final Merger m,
final CodeReviewCommit mergeTip, final CodeReviewCommit n)
throws IOException, OrmException {
args.rw.parseBody(n);
final List<FooterLine> footers = n.getFooterLines();
final StringBuilder msgbuf = new StringBuilder();
msgbuf.append(n.getFullMessage());
if (msgbuf.length() == 0) {
// WTF, an empty commit message?
msgbuf.append("<no commit message provided>");
}
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<PatchSetApproval> approvalList = null;
try {
approvalList =
args.db.patchSetApprovals().byPatchSet(n.patchsetId).toList();
Collections.sort(approvalList, new Comparator<PatchSetApproval>() {
@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<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(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<FooterLine> 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<FooterLine> 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;
}
}

View File

@@ -38,6 +38,9 @@ enum CommitMergeStatus {
/** */ /** */
REVISION_GONE(""), REVISION_GONE(""),
/** */
NO_SUBMIT_TYPE(""),
/** */ /** */
CRISS_CROSS_MERGE("Your change requires a recursive merge to resolve.\n" CRISS_CROSS_MERGE("Your change requires a recursive merge to resolve.\n"
+ "\n" + "\n"

View File

@@ -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<CodeReviewCommit> 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;
}
}

View File

@@ -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<CodeReviewCommit> 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;
}
}

View File

@@ -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<CodeReviewCommit> 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;
}
}

View File

@@ -28,14 +28,14 @@ import java.util.Set;
class MergeSorter { class MergeSorter {
private final RevWalk rw; private final RevWalk rw;
private final RevFlag CAN_MERGE; private final RevFlag canMergeFlag;
private final Set<RevCommit> accepted; private final Set<RevCommit> accepted;
MergeSorter(final RevWalk walk, final Set<RevCommit> alreadyAccepted, MergeSorter(final RevWalk rw, final Set<RevCommit> alreadyAccepted,
final RevFlag flagCAN_MERGE) { final RevFlag canMergeFlag) {
rw = walk; this.rw = rw;
CAN_MERGE = flagCAN_MERGE; this.canMergeFlag = canMergeFlag;
accepted = alreadyAccepted; this.accepted = alreadyAccepted;
} }
Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> incoming) Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> incoming)
@@ -45,7 +45,7 @@ class MergeSorter {
while (!sort.isEmpty()) { while (!sort.isEmpty()) {
final CodeReviewCommit n = removeOne(sort); final CodeReviewCommit n = removeOne(sort);
rw.resetRetain(CAN_MERGE); rw.resetRetain(canMergeFlag);
rw.markStart(n); rw.markStart(n);
for (RevCommit c : accepted) { for (RevCommit c : accepted) {
rw.markUninteresting(c); rw.markUninteresting(c);
@@ -54,7 +54,7 @@ class MergeSorter {
RevCommit c; RevCommit c;
final RevCommitList<RevCommit> contents = new RevCommitList<RevCommit>(); final RevCommitList<RevCommit> contents = new RevCommitList<RevCommit>();
while ((c = rw.next()) != null) { 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 // We cannot merge n as it would bring something we
// aren't permitted to merge at this time. Drop n. // aren't permitted to merge at this time. Drop n.
// //

View File

@@ -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<CodeReviewCommit> toMerge) throws MergeException {
for (final Iterator<CodeReviewCommit> 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<CodeReviewCommit> toSort) throws MergeException {
final Collection<CodeReviewCommit> 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<CodeReviewCommit>() {
@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<PatchSetApproval> 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<CodeReviewCommit> 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<String> emails = new HashSet<String>();
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<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>();
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<CodeReviewCommit> 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<RevCommit> 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);
}
}
}

View File

@@ -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<RevCommit> 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<RevCommit> 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<CodeReviewCommit> 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<CodeReviewCommit> 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 <code>null</code>
*/
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<Change.Id, CodeReviewCommit> 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 <code>true</code> if a merge that failed with
* {@link RefUpdate.Result#LOCK_FAILURE} should be retried, otherwise
* <code>false</code>
*/
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();
}
}
}

View File

@@ -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<String> 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<String> 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<RevCommit> 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);
}
}
}

View File

@@ -16,14 +16,13 @@ package com.google.gerrit.server.project;
import com.google.gerrit.common.data.PermissionRange; import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.SubmitRecord; 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.Account;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb; 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.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.query.change.ChangeData; 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.Provider;
import com.google.inject.util.Providers; 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.IntegerTerm;
import com.googlecode.prolog_cafe.lang.ListTerm; import com.googlecode.prolog_cafe.lang.ListTerm;
import com.googlecode.prolog_cafe.lang.Prolog; 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.StructureTerm;
import com.googlecode.prolog_cafe.lang.Term; import com.googlecode.prolog_cafe.lang.Term;
import com.googlecode.prolog_cafe.lang.VariableTerm;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -303,6 +297,7 @@ public class ChangeControl {
return canSubmit(db, patchSet, null, false, false); return canSubmit(db, patchSet, null, false, false);
} }
@SuppressWarnings("unchecked")
public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet, public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
@Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed) { @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed) {
if (!allowClosed && change.getStatus().isClosed()) { if (!allowClosed && change.getStatus().isClosed()) {
@@ -334,103 +329,18 @@ public class ChangeControl {
return logRuleError("Cannot read patch set " + patchSet.getId(), err); return logRuleError("Cannot read patch set " + patchSet.getId(), err);
} }
List<Term> results = new ArrayList<Term>(); List<Term> results;
Term submitRule; SubmitRuleEvaluator evaluator;
ProjectState projectState = getProjectControl().getProjectState();
PrologEnvironment env;
try { try {
env = projectState.newPrologEnvironment(); evaluator = new SubmitRuleEvaluator(db, patchSet,
} catch (CompileException err) { getProjectControl(),
return logRuleError("Cannot consult rules.pl for " this, change, cd,
+ getProject().getName(), err); fastEvalLabels,
} "locate_submit_rule", "can_submit",
"locate_submit_filter", "filter_submit_results");
try { results = evaluator.evaluate().toJava();
env.set(StoredValues.REVIEW_DB, db); } catch (RuleEvalException e) {
env.set(StoredValues.CHANGE, change); return logRuleError(e.getMessage(), e);
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<Project.NameKey> projectsSeen = new HashSet<Project.NameKey>();
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<? extends Term> 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();
} }
if (results.isEmpty()) { if (results.isEmpty()) {
@@ -438,12 +348,13 @@ public class ChangeControl {
// at least one result informing the caller of the labels that are // at least one result informing the caller of the labels that are
// required for this change to be submittable. Each label will indicate // required for this change to be submittable. Each label will indicate
// whether or not that is actually possible given the permissions. // whether or not that is actually possible given the permissions.
log.error("Submit rule " + submitRule + " for change " + change.getId() log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
+ " of " + getProject().getName() + " has no solution."); + change.getId() + " of " + getProject().getName()
+ " has no solution.");
return ruleError("Project submit rule 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; 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<String> 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<SubmitRecord> logInvalidResult(Term rule, Term record) { private List<SubmitRecord> logInvalidResult(Term rule, Term record) {
return logRuleError("Submit rule " + rule + " for change " + change.getId() return logRuleError("Submit rule " + rule + " for change " + change.getId()
+ " of " + getProject().getName() + " output invalid result: " + record); + " of " + getProject().getName() + " output invalid result: " + record);
@@ -550,6 +520,29 @@ public class ChangeControl {
return Collections.singletonList(rec); 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) { private void appliedBy(SubmitRecord.Label label, Term status) {
if (status.isStructure() && status.arity() == 1) { if (status.isStructure() && status.arity() == 1) {
Term who = status.arg(0); Term who = status.arg(0);

View File

@@ -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);
}
}

View File

@@ -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<Term> results = new ArrayList<Term>();
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<Project.NameKey> projectsSeen = new HashSet<Project.NameKey>();
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<Term> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -140,12 +140,12 @@ not_same(_, _).
:- public can_submit/2. :- public can_submit/2.
%% %%
can_submit(SubmitRule, S) :- can_submit(SubmitRule, S) :-
call_submit_rule(SubmitRule, Tmp), call_rule(SubmitRule, Tmp),
Tmp =.. [submit | Ls], Tmp =.. [submit | Ls],
( is_all_ok(Ls) -> S = ok(Tmp), ! ; S = not_ready(Tmp) ). ( is_all_ok(Ls) -> S = ok(Tmp), ! ; S = not_ready(Tmp) ).
call_submit_rule(P:X, Arg) :- !, F =.. [X, Arg], P:F. call_rule(P:X, Arg) :- !, F =.. [X, Arg], P:F.
call_submit_rule(X, Arg) :- !, F =.. [X, Arg], F. call_rule(X, Arg) :- !, F =.. [X, Arg], F.
is_all_ok([]). is_all_ok([]).
is_all_ok([label(_, ok(__)) | Ls]) :- is_all_ok(Ls). 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. 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: %% locate_submit_rule/1:
@@ -164,17 +179,32 @@ is_all_ok(_) :- fail.
%% %%
locate_submit_rule(RuleName) :- locate_submit_rule(RuleName) :-
'$compiled_predicate'(user, submit_rule, 1), locate_helper(submit_rule, default_submit, 1, RuleName).
!,
RuleName = user:submit_rule
. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
locate_submit_rule(RuleName) :- %%
clause(user:submit_rule(_), _), %% get_submit_type/2:
!, %%
RuleName = user:submit_rule %% Executes the SubmitTypeRule and return the first solution
. %%
locate_submit_rule(RuleName) :- :- public get_submit_type/2.
RuleName = gerrit:default_submit. %%
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(P:X, R, S) :- !, F =.. [X, R, S], P:F.
call_submit_filter(X, R, S) :- F =.. [X, R, S], 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. :- public locate_submit_filter/1.
%% %%
locate_submit_filter(FilterName) :- locate_submit_filter(FilterName) :-
'$compiled_predicate'(user, submit_filter, 2), locate_helper(submit_filter, noop_filter, 2, FilterName).
!,
FilterName = user:submit_filter
.
locate_submit_filter(FilterName) :-
clause(user:submit_filter(_,_), _),
FilterName = user:submit_filter
.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
%% 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).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% %%