Replace all transactions with single row updates

We now try to perform writes in a reasonably sane order, but only
update one record at a time (or do a batch against the same table
but not in a transaction).

This matches with eventually consistent databases like Cassandra
where we can't rely upon a transaction being available for any
sort of update we make.

Change-Id: Ib8cdff05f08467dfe3258f03b72d5cabb5b2641f
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2009-12-31 14:25:56 -08:00
parent b1dac0a73b
commit 113860696a
24 changed files with 858 additions and 882 deletions

View File

@@ -36,8 +36,10 @@ import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.ChangeMessage;
import com.google.gerrit.reviewdb.ContributorAgreement;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetAncestor;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.PatchSetInfo;
import com.google.gerrit.reviewdb.RevId;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
@@ -45,16 +47,14 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.Nullable;
import com.google.gerrit.server.git.PatchSetImporter;
import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gwtorm.client.AtomicUpdate;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.OrmRunnable;
import com.google.gwtorm.client.Transaction;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.MissingObjectException;
@@ -150,9 +150,6 @@ final class Receive extends AbstractGitCommand {
@Inject
private ReplicationQueue replication;
@Inject
private PatchSetImporter.Factory importFactory;
@Inject
private PatchSetInfoFactory patchSetInfoFactory;
@@ -186,7 +183,8 @@ final class Receive extends AbstractGitCommand {
protected void runImpl() throws IOException, Failure {
if (!canUpload()) {
final String reqName = project.getName();
throw new Failure(1, "fatal: Upload denied for project '" + reqName + "'",
throw new Failure(1,
"fatal: Upload denied for project '" + reqName + "'",
new SecurityException("Account lacks Upload permission"));
}
@@ -809,20 +807,21 @@ final class Receive extends AbstractGitCommand {
cc.remove(me);
cc.removeAll(reviewers);
final Transaction txn = db.beginTransaction();
final Change change =
new Change(changeKey, new Change.Id(db.nextChangeId()), me, destBranch);
final PatchSet ps = new PatchSet(change.newPatchSetId());
change.nextPatchSetId();
final PatchSet ps = new PatchSet(change.currPatchSetId());
ps.setCreatedOn(change.getCreatedOn());
ps.setUploader(me);
ps.setRevision(toRevId(c));
insertAncestors(ps.getId(), c);
db.patchSets().insert(Collections.singleton(ps));
final PatchSetImporter imp = importFactory.create(db, c, ps, true);
imp.setTransaction(txn);
imp.run();
change.setCurrentPatchSet(imp.getPatchSetInfo());
final PatchSetInfo info = patchSetInfoFactory.get(c, ps.getId());
change.setCurrentPatchSet(info);
ChangeUtil.updated(change);
db.changes().insert(Collections.singleton(change), txn);
db.changes().insert(Collections.singleton(change));
final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
@@ -830,28 +829,24 @@ final class Receive extends AbstractGitCommand {
if (allTypes.size() > 0) {
final Account.Id authorId =
imp.getPatchSetInfo().getAuthor() != null ? imp.getPatchSetInfo()
.getAuthor().getAccount() : null;
info.getAuthor() != null ? info.getAuthor().getAccount() : null;
final Account.Id committerId =
imp.getPatchSetInfo().getCommitter() != null ? imp.getPatchSetInfo()
.getCommitter().getAccount() : null;
info.getCommitter() != null ? info.getCommitter().getAccount() : null;
final ApprovalCategory.Id catId =
allTypes.get(allTypes.size() - 1).getCategory().getId();
if (authorId != null && haveApprovals.add(authorId)) {
insertDummyApproval(change, ps.getId(), authorId, catId, db, txn);
insertDummyApproval(change, ps.getId(), authorId, catId, db);
}
if (committerId != null && haveApprovals.add(committerId)) {
insertDummyApproval(change, ps.getId(), committerId, catId, db, txn);
insertDummyApproval(change, ps.getId(), committerId, catId, db);
}
for (final Account.Id reviewer : reviewers) {
if (haveApprovals.add(reviewer)) {
insertDummyApproval(change, ps.getId(), reviewer, catId, db, txn);
insertDummyApproval(change, ps.getId(), reviewer, catId, db);
}
}
}
txn.commit();
final RefUpdate ru = repo.updateRef(ps.getRefName());
ru.setNewObjectId(c);
ru.disableRefLog();
@@ -867,7 +862,7 @@ final class Receive extends AbstractGitCommand {
final CreateChangeSender cm;
cm = createChangeSenderFactory.create(change);
cm.setFrom(me);
cm.setPatchSet(ps, imp.getPatchSetInfo());
cm.setPatchSet(ps, info);
cm.setReviewDb(db);
cm.addReviewers(reviewers);
cm.addExtraCC(cc);
@@ -932,221 +927,238 @@ final class Receive extends AbstractGitCommand {
cc.remove(me);
cc.removeAll(reviewers);
final ReplaceResult result;
final ReplaceResult result = new ReplaceResult();
final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
final Set<Account.Id> oldCC = new HashSet<Account.Id>();
result = db.run(new OrmRunnable<ReplaceResult, ReviewDb>() {
public ReplaceResult run(final ReviewDb db, final Transaction txn,
final boolean isRetry) throws OrmException {
final Change change = db.changes().get(request.ontoChange);
if (change == null) {
reject(request.cmd, "change " + request.ontoChange + " not found");
return null;
}
if (change.getStatus().isClosed()) {
reject(request.cmd, "change " + request.ontoChange + " closed");
return null;
}
Change change = db.changes().get(request.ontoChange);
if (change == null) {
reject(request.cmd, "change " + request.ontoChange + " not found");
return null;
}
if (change.getStatus().isClosed()) {
reject(request.cmd, "change " + request.ontoChange + " closed");
return null;
}
final PatchSet.Id priorPatchSet = change.currentPatchSetId();
for (final PatchSet ps : db.patchSets().byChange(request.ontoChange)) {
if (ps.getRevision() == null) {
reject(request.cmd, "change state corrupt");
return null;
}
final String revIdStr = ps.getRevision().get();
final ObjectId commitId;
try {
commitId = ObjectId.fromString(revIdStr);
} catch (IllegalArgumentException e) {
log.warn("Invalid revision in " + ps.getId() + ": " + revIdStr);
reject(request.cmd, "change state corrupt");
return null;
}
try {
final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
// Don't allow a change to directly depend upon itself. This is a
// very common error due to users making a new commit rather than
// amending when trying to address review comments.
//
if (rp.getRevWalk().isMergedInto(prior, c)) {
reject(request.cmd, "squash commits first");
return null;
}
// Don't allow the same commit to appear twice on the same change
//
if (c == prior) {
reject(request.cmd, "commit already exists");
return null;
}
// Don't allow the same tree if the commit message is unmodified
// or no parents were updated (rebase), else warn that only part
// of the commit was modified.
//
if (priorPatchSet.equals(ps.getId())
&& c.getTree() == prior.getTree()) {
rp.getRevWalk().parseBody(prior);
final boolean messageEq =
c.getFullMessage().equals(prior.getFullMessage());
final boolean parentsEq = parentsEqual(c, prior);
if (messageEq && parentsEq) {
reject(request.cmd, "no changes made");
return null;
} else {
err.write(Constants.encode("warning: " //
+ change.getKey().abbreviate() + ": " //
+ " no files changed, but" //
+ (!messageEq ? " message updated" : "") //
+ (!messageEq && !parentsEq ? " and" : "") //
+ (!parentsEq ? " was rebased" : "") //
+ "\n" //
));
}
}
} catch (IOException e) {
log.error("Change " + change.getId() + " missing " + revIdStr, e);
reject(request.cmd, "change state corrupt");
return null;
}
}
final PatchSet ps = new PatchSet(change.newPatchSetId());
ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
ps.setUploader(currentUser.getAccountId());
final PatchSetImporter imp = importFactory.create(db, c, ps, true);
imp.setTransaction(txn);
imp.run();
final Ref mergedInto = findMergedInto(change.getDest().get(), c);
final ReplaceResult result = new ReplaceResult();
result.mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
result.change = change;
result.patchSet = ps;
result.info = imp.getPatchSetInfo();
final Account.Id authorId =
imp.getPatchSetInfo().getAuthor() != null ? imp.getPatchSetInfo()
.getAuthor().getAccount() : null;
final Account.Id committerId =
imp.getPatchSetInfo().getCommitter() != null ? imp
.getPatchSetInfo().getCommitter().getAccount() : null;
boolean haveAuthor = false;
boolean haveCommitter = false;
final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
oldReviewers.clear();
oldCC.clear();
for (PatchSetApproval a : db.patchSetApprovals().byChange(
change.getId())) {
haveApprovals.add(a.getAccountId());
if (a.getValue() != 0) {
oldReviewers.add(a.getAccountId());
} else {
oldCC.add(a.getAccountId());
}
final ApprovalType type =
approvalTypes.getApprovalType(a.getCategoryId());
if (a.getPatchSetId().equals(priorPatchSet)
&& type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
// If there was a negative vote on the prior patch set, carry it
// into this patch set.
//
db.patchSetApprovals()
.insert(
Collections.singleton(new PatchSetApproval(ps.getId(), a)),
txn);
}
if (!haveAuthor && authorId != null
&& a.getAccountId().equals(authorId)) {
haveAuthor = true;
}
if (!haveCommitter && committerId != null
&& a.getAccountId().equals(committerId)) {
haveCommitter = true;
}
}
final ChangeMessage msg =
new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
.messageUUID(db)), me, ps.getCreatedOn());
msg.setMessage("Uploaded patch set " + ps.getPatchSetId() + ".");
db.changeMessages().insert(Collections.singleton(msg), txn);
result.msg = msg;
if (result.mergedIntoRef != null) {
// Change was already submitted to a branch, close it.
//
markChangeMergedByPush(db, txn, result);
} else {
// Change should be new, so it can go through review again.
//
change.setStatus(Change.Status.NEW);
change.setCurrentPatchSet(imp.getPatchSetInfo());
ChangeUtil.updated(change);
db.changes().update(Collections.singleton(change), txn);
}
final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
if (allTypes.size() > 0) {
final ApprovalCategory.Id catId =
allTypes.get(allTypes.size() - 1).getCategory().getId();
if (authorId != null && haveApprovals.add(authorId)) {
insertDummyApproval(result, authorId, catId, db, txn);
}
if (committerId != null && haveApprovals.add(committerId)) {
insertDummyApproval(result, committerId, catId, db, txn);
}
for (final Account.Id reviewer : reviewers) {
if (haveApprovals.add(reviewer)) {
insertDummyApproval(result, reviewer, catId, db, txn);
}
}
}
return result;
final PatchSet.Id priorPatchSet = change.currentPatchSetId();
for (final PatchSet ps : db.patchSets().byChange(request.ontoChange)) {
if (ps.getRevision() == null) {
reject(request.cmd, "change state corrupt");
return null;
}
});
if (result != null) {
final PatchSet ps = result.patchSet;
final RefUpdate ru = repo.updateRef(ps.getRefName());
ru.setNewObjectId(c);
ru.disableRefLog();
if (ru.update(rp.getRevWalk()) != RefUpdate.Result.NEW) {
throw new IOException("Failed to create ref " + ps.getRefName()
+ " in " + repo.getDirectory() + ": " + ru.getResult());
final String revIdStr = ps.getRevision().get();
final ObjectId commitId;
try {
commitId = ObjectId.fromString(revIdStr);
} catch (IllegalArgumentException e) {
log.warn("Invalid revision in " + ps.getId() + ": " + revIdStr);
reject(request.cmd, "change state corrupt");
return null;
}
replication.scheduleUpdate(project.getNameKey(), ru.getName());
request.cmd.setResult(ReceiveCommand.Result.OK);
try {
final ReplacePatchSetSender cm;
cm = replacePatchSetFactory.create(result.change);
cm.setFrom(me);
cm.setPatchSet(ps, result.info);
cm.setChangeMessage(result.msg);
cm.setReviewDb(db);
cm.addReviewers(reviewers);
cm.addExtraCC(cc);
cm.addReviewers(oldReviewers);
cm.addExtraCC(oldCC);
cm.send();
} catch (EmailException e) {
log.error("Cannot send email for new patch set " + ps.getId(), e);
final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
// Don't allow a change to directly depend upon itself. This is a
// very common error due to users making a new commit rather than
// amending when trying to address review comments.
//
if (rp.getRevWalk().isMergedInto(prior, c)) {
reject(request.cmd, "squash commits first");
return null;
}
// Don't allow the same commit to appear twice on the same change
//
if (c == prior) {
reject(request.cmd, "commit already exists");
return null;
}
// Don't allow the same tree if the commit message is unmodified
// or no parents were updated (rebase), else warn that only part
// of the commit was modified.
//
if (priorPatchSet.equals(ps.getId()) && c.getTree() == prior.getTree()) {
rp.getRevWalk().parseBody(prior);
final boolean messageEq =
c.getFullMessage().equals(prior.getFullMessage());
final boolean parentsEq = parentsEqual(c, prior);
if (messageEq && parentsEq) {
reject(request.cmd, "no changes made");
return null;
} else {
err.write(Constants.encode("warning: " //
+ change.getKey().abbreviate() + ": " //
+ " no files changed, but" //
+ (!messageEq ? " message updated" : "") //
+ (!messageEq && !parentsEq ? " and" : "") //
+ (!parentsEq ? " was rebased" : "") //
+ "\n" //
));
}
}
} catch (IOException e) {
log.error("Change " + change.getId() + " missing " + revIdStr, e);
reject(request.cmd, "change state corrupt");
return null;
}
}
change =
db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (change.getStatus().isOpen()) {
change.nextPatchSetId();
return change;
} else {
return null;
}
}
});
if (change == null) {
reject(request.cmd, "change is closed");
return null;
}
final PatchSet ps = new PatchSet(change.currPatchSetId());
ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
ps.setUploader(currentUser.getAccountId());
ps.setRevision(toRevId(c));
insertAncestors(ps.getId(), c);
db.patchSets().insert(Collections.singleton(ps));
final Ref mergedInto = findMergedInto(change.getDest().get(), c);
result.mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
result.change = change;
result.patchSet = ps;
result.info = patchSetInfoFactory.get(c, ps.getId());
final Account.Id authorId =
result.info.getAuthor() != null ? result.info.getAuthor().getAccount()
: null;
final Account.Id committerId =
result.info.getCommitter() != null ? result.info.getCommitter()
.getAccount() : null;
boolean haveAuthor = false;
boolean haveCommitter = false;
final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
oldReviewers.clear();
oldCC.clear();
for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) {
haveApprovals.add(a.getAccountId());
if (a.getValue() != 0) {
oldReviewers.add(a.getAccountId());
} else {
oldCC.add(a.getAccountId());
}
final ApprovalType type =
approvalTypes.getApprovalType(a.getCategoryId());
if (a.getPatchSetId().equals(priorPatchSet)
&& type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
// If there was a negative vote on the prior patch set, carry it
// into this patch set.
//
db.patchSetApprovals().insert(
Collections.singleton(new PatchSetApproval(ps.getId(), a)));
}
if (!haveAuthor && authorId != null && a.getAccountId().equals(authorId)) {
haveAuthor = true;
}
if (!haveCommitter && committerId != null
&& a.getAccountId().equals(committerId)) {
haveCommitter = true;
}
}
final ChangeMessage msg =
new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
.messageUUID(db)), me, ps.getCreatedOn());
msg.setMessage("Uploaded patch set " + ps.getPatchSetId() + ".");
db.changeMessages().insert(Collections.singleton(msg));
result.msg = msg;
if (result.mergedIntoRef != null) {
// Change was already submitted to a branch, close it.
//
markChangeMergedByPush(db, result);
} else {
// Change should be new, so it can go through review again.
//
change =
db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (change.getStatus().isOpen()) {
change.setStatus(Change.Status.NEW);
change.setCurrentPatchSet(result.info);
ChangeUtil.updated(change);
return change;
} else {
return null;
}
}
});
if (change == null) {
db.patchSets().delete(Collections.singleton(ps));
db.changeMessages().delete(Collections.singleton(msg));
reject(request.cmd, "change is closed");
return null;
}
}
final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
if (allTypes.size() > 0) {
final ApprovalCategory.Id catId =
allTypes.get(allTypes.size() - 1).getCategory().getId();
if (authorId != null && haveApprovals.add(authorId)) {
insertDummyApproval(result, authorId, catId, db);
}
if (committerId != null && haveApprovals.add(committerId)) {
insertDummyApproval(result, committerId, catId, db);
}
for (final Account.Id reviewer : reviewers) {
if (haveApprovals.add(reviewer)) {
insertDummyApproval(result, reviewer, catId, db);
}
}
}
final RefUpdate ru = repo.updateRef(ps.getRefName());
ru.setNewObjectId(c);
ru.disableRefLog();
if (ru.update(rp.getRevWalk()) != RefUpdate.Result.NEW) {
throw new IOException("Failed to create ref " + ps.getRefName() + " in "
+ repo.getDirectory() + ": " + ru.getResult());
}
replication.scheduleUpdate(project.getNameKey(), ru.getName());
request.cmd.setResult(ReceiveCommand.Result.OK);
try {
final ReplacePatchSetSender cm;
cm = replacePatchSetFactory.create(result.change);
cm.setFrom(me);
cm.setPatchSet(ps, result.info);
cm.setChangeMessage(result.msg);
cm.setReviewDb(db);
cm.addReviewers(reviewers);
cm.addExtraCC(cc);
cm.addReviewers(oldReviewers);
cm.addExtraCC(oldCC);
cm.send();
} catch (EmailException e) {
log.error("Cannot send email for new patch set " + ps.getId(), e);
}
sendMergedEmail(result);
return result != null ? result.info.getKey() : null;
}
@@ -1165,19 +1177,19 @@ final class Receive extends AbstractGitCommand {
private void insertDummyApproval(final ReplaceResult result,
final Account.Id forAccount, final ApprovalCategory.Id catId,
final ReviewDb db, final Transaction txn) throws OrmException {
final ReviewDb db) throws OrmException {
insertDummyApproval(result.change, result.patchSet.getId(), forAccount,
catId, db, txn);
catId, db);
}
private void insertDummyApproval(final Change change, final PatchSet.Id psId,
final Account.Id forAccount, final ApprovalCategory.Id catId,
final ReviewDb db, final Transaction txn) throws OrmException {
final ReviewDb db) throws OrmException {
final PatchSetApproval ca =
new PatchSetApproval(new PatchSetApproval.Key(psId, forAccount, catId),
(short) 0);
ca.cache(change);
db.patchSetApprovals().insert(Collections.singleton(ca), txn);
db.patchSetApprovals().insert(Collections.singleton(ca));
}
private Ref findMergedInto(final String first, final RevCommit commit) {
@@ -1327,35 +1339,29 @@ final class Receive extends AbstractGitCommand {
final RevCommit commit) throws OrmException {
final String refName = cmd.getRefName();
final Change.Id cid = psi.getParentKey();
final ReplaceResult result =
db.run(new OrmRunnable<ReplaceResult, ReviewDb>() {
@Override
public ReplaceResult run(ReviewDb db, Transaction txn, boolean retry)
throws OrmException {
final Change change = db.changes().get(cid);
final PatchSet ps = db.patchSets().get(psi);
if (change == null || ps == null) {
log.warn(project.getName() + " " + psi + " is missing");
return null;
}
if (change.getStatus() == Change.Status.MERGED) {
// If its already merged, don't make further updates, it
// might just be moving from an experimental branch into
// a more stable branch.
//
return null;
}
final Change change = db.changes().get(cid);
final PatchSet ps = db.patchSets().get(psi);
if (change == null || ps == null) {
log.warn(project.getName() + " " + psi + " is missing");
return;
}
final ReplaceResult result = new ReplaceResult();
result.change = change;
result.patchSet = ps;
result.info = patchSetInfoFactory.get(commit, psi);
result.mergedIntoRef = refName;
markChangeMergedByPush(db, txn, result);
return result;
}
});
if (change.getStatus() == Change.Status.MERGED) {
// If its already merged, don't make further updates, it
// might just be moving from an experimental branch into
// a more stable branch.
//
return;
}
final ReplaceResult result = new ReplaceResult();
result.change = change;
result.patchSet = ps;
result.info = patchSetInfoFactory.get(commit, psi);
result.mergedIntoRef = refName;
markChangeMergedByPush(db, result);
sendMergedEmail(result);
}
@@ -1379,7 +1385,7 @@ final class Receive extends AbstractGitCommand {
return r;
}
private void markChangeMergedByPush(final ReviewDb db, final Transaction txn,
private void markChangeMergedByPush(final ReviewDb db,
final ReplaceResult result) throws OrmException {
final Change change = result.change;
final String mergedIntoRef = result.mergedIntoRef;
@@ -1393,6 +1399,7 @@ final class Receive extends AbstractGitCommand {
for (PatchSetApproval a : approvals) {
a.cache(change);
}
db.patchSetApprovals().update(approvals);
final StringBuilder msgBuf = new StringBuilder();
msgBuf.append("Change has been successfully pushed");
@@ -1411,9 +1418,19 @@ final class Receive extends AbstractGitCommand {
.messageUUID(db)), currentUser.getAccountId());
msg.setMessage(msgBuf.toString());
db.patchSetApprovals().update(approvals, txn);
db.changeMessages().insert(Collections.singleton(msg), txn);
db.changes().update(Collections.singleton(change), txn);
db.changeMessages().insert(Collections.singleton(msg));
db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (change.getStatus().isOpen()) {
change.setCurrentPatchSet(result.info);
change.setStatus(Change.Status.MERGED);
ChangeUtil.updated(change);
}
return change;
}
});
}
private void sendMergedEmail(final ReplaceResult result) {
@@ -1433,6 +1450,24 @@ final class Receive extends AbstractGitCommand {
}
}
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(toRevId(src.getParent(p)));
toInsert.add(a);
}
db.patchSetAncestors().insert(toInsert);
}
private static RevId toRevId(final RevCommit src) {
return new RevId(src.getId().name());
}
private boolean canPerform(final ApprovalCategory.Id actionId, final short val) {
return projectControl.canPerform(actionId, val);
}