Refactor MergeOp: implement each submit strategy in an own class

This is the second step in the MergeOp refactoring. With this change
an abstraction for a submit strategy is introduced. There is a new
abstract base class that each submit strategy has to extend. By having
polymorphism for the submit type the coding for the different submit
strategies gets seperated. This makes it much easier to add a new
submit strategy because only a clearly defined interface has to be
implemented.

Change-Id: I5afe9fdc3e4c5a796cc2be715194effed9e34ddf
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2012-09-25 14:41:36 +02:00
parent 2b3bafa470
commit 7e136c540a
7 changed files with 822 additions and 391 deletions

View File

@@ -0,0 +1,374 @@
// 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.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

@@ -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,51 @@
// 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.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,54 @@
// 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.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

@@ -14,15 +14,8 @@
package com.google.gerrit.server.git;
import static com.google.gerrit.server.git.MergeUtil.commit;
import static com.google.gerrit.server.git.MergeUtil.computeMergeCommitAuthor;
import static com.google.gerrit.server.git.MergeUtil.getFirstFastForward;
import static com.google.gerrit.server.git.MergeUtil.getSubmitter;
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 static com.google.gerrit.server.git.MergeUtil.reduceToMinimalMerge;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -36,7 +29,6 @@ import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
@@ -45,7 +37,6 @@ import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
@@ -63,13 +54,11 @@ import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -77,10 +66,6 @@ import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.Merger;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevSort;
@@ -89,10 +74,8 @@ 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.HashSet;
import java.util.List;
@@ -100,8 +83,6 @@ import java.util.Map;
import java.util.Random;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Merges changes in submission order into a single branch.
* <p>
@@ -122,12 +103,6 @@ public class MergeOp {
}
private static final Logger log = LoggerFactory.getLogger(MergeOp.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");
/** Amount of time to wait between submit and checking for missing deps. */
private static final long DEPENDENCY_DELAY =
@@ -142,7 +117,6 @@ public class MergeOp {
private final GitReferenceUpdated replication;
private final MergedSender.Factory mergedSenderFactory;
private final MergeFailSender.Factory mergeFailSenderFactory;
private final Provider<String> urlProvider;
private final ApprovalTypes approvalTypes;
private final PatchSetInfoFactory patchSetInfoFactory;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
@@ -164,11 +138,13 @@ public class MergeOp {
private Set<RevCommit> alreadyAccepted;
private RefUpdate branchUpdate;
private ObjectInserter inserter;
private PersonIdent refLogIdent;
private final ChangeHooks hooks;
private final AccountCache accountCache;
private final TagCache tagCache;
private final CreateCodeReviewNotes.Factory codeReviewNotesFactory;
private final SubmitStrategyFactory submitStrategyFactory;
private final SubmoduleOp.Factory subOpFactory;
private final WorkQueue workQueue;
private final RequestScopePropagator requestScopePropagator;
@@ -178,7 +154,6 @@ public class MergeOp {
final ProjectCache pc, final FunctionState.Factory fs,
final GitReferenceUpdated rq, final MergedSender.Factory msf,
final MergeFailSender.Factory mfsf,
@CanonicalWebUrl @Nullable final Provider<String> cwu,
final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
final IdentifiedUser.GenericFactory iuf,
final ChangeControl.GenericFactory changeControlFactory,
@@ -186,6 +161,7 @@ public class MergeOp {
final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
final ChangeHooks hooks, final AccountCache accountCache,
final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf,
final SubmitStrategyFactory submitStrategyFactory,
final SubmoduleOp.Factory subOpFactory,
final WorkQueue workQueue,
final RequestScopePropagator requestScopePropagator) {
@@ -196,7 +172,6 @@ public class MergeOp {
replication = rq;
mergedSenderFactory = msf;
mergeFailSenderFactory = mfsf;
urlProvider = cwu;
this.approvalTypes = approvalTypes;
patchSetInfoFactory = psif;
identifiedUserFactory = iuf;
@@ -206,6 +181,7 @@ public class MergeOp {
this.accountCache = accountCache;
this.tagCache = tagCache;
codeReviewNotesFactory = crnf;
this.submitStrategyFactory = submitStrategyFactory;
this.subOpFactory = subOpFactory;
this.workQueue = workQueue;
this.requestScopePropagator = requestScopePropagator;
@@ -231,7 +207,7 @@ public class MergeOp {
|| (destBranchRef != null && !destBranchRef.getObjectId().getName()
.equals(change.getLastSha1MergeTested().get()))) {
openSchema();
preMerge();
preMerge(createStrategy());
// update sha1 tested merge.
if (destBranchRef != null) {
@@ -282,8 +258,9 @@ public class MergeOp {
openSchema();
openRepository();
submitted = db.changes().submitted(destBranch).toList();
preMerge();
updateBranch();
final SubmitStrategy strategy = createStrategy();
preMerge(strategy);
updateBranch(strategy);
updateChangeStatus();
updateSubscriptions();
} catch (OrmException e) {
@@ -304,27 +281,18 @@ public class MergeOp {
}
}
private void preMerge() throws MergeException, OrmException {
private void preMerge(final SubmitStrategy strategy) throws MergeException {
openBranch();
validateChangeList();
mergeTip = branchTip;
switch (destProject.getSubmitType()) {
case CHERRY_PICK:
cherryPickChanges();
break;
mergeTip = strategy.run(branchTip, toMerge);
refLogIdent = strategy.getRefLogIdent();
commits.putAll(strategy.getNewCommits());
}
case FAST_FORWARD_ONLY:
case MERGE_ALWAYS:
case MERGE_IF_NECESSARY:
default:
reduceToMinimalMerge(new MergeSorter(rw, alreadyAccepted, CAN_MERGE),
toMerge);
mergeTopics();
final PatchSetApproval submitApproval =
markCleanMerges(db, rw, CAN_MERGE, mergeTip, alreadyAccepted);
setRefLogIdent(submitApproval);
break;
}
private SubmitStrategy createStrategy() throws MergeException {
return submitStrategyFactory.create(destProject.getSubmitType(), db, repo,
rw, inserter, CAN_MERGE, alreadyAccepted, destBranch,
destProject.isUseContentMerge());
}
private void openRepository() throws MergeException {
@@ -483,335 +451,8 @@ public class MergeOp {
}
}
private void mergeTopics() throws MergeException {
// Take the first fast-forward available, if any is available in the set.
//
if (destProject.getSubmitType() != Project.SubmitType.MERGE_ALWAYS) {
mergeTip = getFirstFastForward(mergeTip, rw, toMerge);
}
if (destProject.getSubmitType() == Project.SubmitType.FAST_FORWARD_ONLY) {
// If this project only permits fast-forwards, abort everything else.
//
while (!toMerge.isEmpty()) {
final CodeReviewCommit n = toMerge.remove(0);
n.statusCode = CommitMergeStatus.NOT_FAST_FORWARD;
}
} else {
// For every other commit do a pair-wise merge.
//
while (!toMerge.isEmpty()) {
mergeTip =
mergeOneCommit(db, identifiedUserFactory, myIdent, repo, rw,
inserter, destProject.isUseContentMerge(), destBranch,
mergeTip, toMerge.remove(0));
}
}
}
private void setRefLogIdent(final PatchSetApproval submitAudit) {
if (submitAudit != null) {
branchUpdate.setRefLogIdent(identifiedUserFactory.create(
submitAudit.getAccountId()).newRefLogIdent());
}
}
private void cherryPickChanges() throws MergeException, OrmException {
while (!toMerge.isEmpty()) {
final CodeReviewCommit n = toMerge.remove(0);
final ThreeWayMerger m = newThreeWayMerger(repo, inserter, destProject.isUseContentMerge());
try {
if (mergeTip == null) {
// The branch is unborn. Take a fast-forward resolution to
// create the branch.
//
mergeTip = n;
n.statusCode = CommitMergeStatus.CLEAN_MERGE;
} else if (n.getParentCount() == 0) {
// Refuse to merge a root commit into an existing branch,
// we cannot obtain a delta for the cherry-pick to apply.
//
n.statusCode = CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT;
} else if (n.getParentCount() == 1) {
// If there is only one parent, a cherry-pick can be done by
// taking the delta relative to that one parent and redoing
// that on the current merge tip.
//
m.setBase(n.getParent(0));
if (m.merge(mergeTip, n)) {
writeCherryPickCommit(m, n);
} else {
n.statusCode = CommitMergeStatus.PATH_CONFLICT;
}
} else {
// There are multiple parents, so this is a merge commit. We
// don't want to cherry-pick the merge as clients can't easily
// rebase their history with that merge present and replaced
// by an equivalent merge with a different first parent. So
// instead behave as though MERGE_IF_NECESSARY was configured.
//
if (!hasMissingDependencies(new MergeSorter(rw, alreadyAccepted, CAN_MERGE), n)) {
if (rw.isMergedInto(mergeTip, n)) {
mergeTip = n;
} else {
mergeTip =
mergeOneCommit(db, identifiedUserFactory, myIdent, repo, rw,
inserter, destProject.isUseContentMerge(), destBranch,
mergeTip, n);
}
final PatchSetApproval submitApproval =
markCleanMerges(db, rw, CAN_MERGE, mergeTip, 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);
}
}
}
private void writeCherryPickCommit(final Merger m, final CodeReviewCommit n)
throws IOException, OrmException {
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 =
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 =
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(inserter, mergeCommit);
final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
final Change oldChange = n.change;
n.change =
db.changes().atomicUpdate(n.change.getId(),
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
change.nextPatchSetId();
return change;
}
});
final PatchSet ps = new PatchSet(n.change.currPatchSetId());
ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
ps.setUploader(submitAudit.getAccountId());
ps.setRevision(new RevId(id.getName()));
insertAncestors(ps.getId(), newCommit);
db.patchSets().insert(Collections.singleton(ps));
n.change =
db.changes().atomicUpdate(n.change.getId(),
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit,
ps.getId()));
return change;
}
});
this.submitted.remove(oldChange);
this.submitted.add(n.change);
if (approvalList != null) {
for (PatchSetApproval a : approvalList) {
db.patchSetApprovals().insert(
Collections.singleton(new PatchSetApproval(ps.getId(), a)));
}
}
final RefUpdate ru = repo.updateRef(ps.getRefName());
ru.setExpectedOldObjectId(ObjectId.zeroId());
ru.setNewObjectId(newCommit);
ru.disableRefLog();
if (ru.update(rw) != RefUpdate.Result.NEW) {
throw new IOException(String.format(
"Failed to create ref %s in %s: %s", ps.getRefName(),
n.change.getDest().getParentKey().get(), ru.getResult()));
}
replication.fire(n.change.getProject(), ru.getName());
newCommit.copyFrom(n);
newCommit.statusCode = CommitMergeStatus.CLEAN_PICK;
commits.put(newCommit.patchsetId.getParentKey(), newCommit);
mergeTip = newCommit;
setRefLogIdent(submitAudit);
}
private void insertAncestors(PatchSet.Id id, RevCommit src)
throws OrmException {
final int cnt = src.getParentCount();
List<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);
}
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 identifiedUserFactory.create(audit.getAccountId())
.newCommitterIdent(audit.getGranted(), myIdent.getTimeZone());
}
return myIdent;
}
private void updateBranch() throws MergeException {
private void updateBranch(final SubmitStrategy strategy)
throws MergeException {
if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) {
if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) {
try {
@@ -824,6 +465,7 @@ public class MergeOp {
}
}
branchUpdate.setRefLogIdent(refLogIdent);
branchUpdate.setForceUpdate(false);
branchUpdate.setNewObjectId(mergeTip);
branchUpdate.setRefLogMessage("merged", true);
@@ -856,19 +498,9 @@ public class MergeOp {
break;
case LOCK_FAILURE:
switch (destProject.getSubmitType()) {
case CHERRY_PICK:
case MERGE_ALWAYS:
case MERGE_IF_NECESSARY:
mergeQueue.recheckAfter(destBranch, random.nextInt(1000), MILLISECONDS);
break;
case FAST_FORWARD_ONLY:
break; // Not mergeable, no need to recheck.
default:
log.warn("Lock failure in project with unknown merge type "
+ destProject.getSubmitType());
break;
if (strategy.retryOnLockFailure()) {
mergeQueue.recheckAfter(destBranch, random.nextInt(1000),
MILLISECONDS);
}
break;
default:

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