ChangeNotes: Enforce project passed to factory matches change

In Ib164511e20 we started passing project to ChangeData.Factory
methods. Unfortunately during review we missed the fact that we're
passing the wrong project to the factory in
Submit#problemsForSubmittingChanges. This has subtle and unfortunate
consequences: using the wrong repository for the mergeability check
will always fail, since the commit to test does not belong to that
repository. The end result is that submitWholeTopic across projects
was intermittently broken, since we could never get the mergeability
check to pass on any change not in the same project as the one where
submit was clicked.

(Except that, maddeningly, when commits are shared between
projects using submitWholeTopic, as was the case before I71efad9d06,
the mergeability checks would succeed anyway, making tests pass when
they shouldn't have.)

Add a sanity check in this factory method. Fix the issue in Submit by
just using the existing ChangeData from the ChangeSet, which it's
unclear why we weren't doing that in the first place. To do so we also
need to teach MergeSuperSet to prepopulate the ChangeControls with the
right user, which are used for visibility checks later.

Change-Id: I3fa88121dde5b5eaaf72ead916466cf6d3e3c9f1
This commit is contained in:
Dave Borowitz
2016-02-18 20:49:54 -05:00
parent 1a245bda61
commit 3e4b231afc
7 changed files with 41 additions and 30 deletions

View File

@@ -68,7 +68,8 @@ public class GetRevisionActions implements ETagView<RevisionResource> {
rsrc.getChangeResource().prepareETag(h, user); rsrc.getChangeResource().prepareETag(h, user);
h.putBoolean(Submit.wholeTopicEnabled(config)); h.putBoolean(Submit.wholeTopicEnabled(config));
ReviewDb db = dbProvider.get(); ReviewDb db = dbProvider.get();
ChangeSet cs = mergeSuperSet.completeChangeSet(db, rsrc.getChange()); ChangeSet cs =
mergeSuperSet.completeChangeSet(db, rsrc.getChange(), user);
for (ChangeData cd : cs.changes()) { for (ChangeData cd : cs.changes()) {
new ChangeResource(cd.changeControl()).prepareETag(h, user); new ChangeResource(cd.changeControl()).prepareETag(h, user);
} }

View File

@@ -32,7 +32,6 @@ import com.google.gerrit.extensions.webui.UiAction;
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 com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.ChangeMessagesUtil;
@@ -47,7 +46,6 @@ import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeSuperSet; import com.google.gerrit.server.git.MergeSuperSet;
import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
@@ -117,7 +115,6 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
private final GitRepositoryManager repoManager; private final GitRepositoryManager repoManager;
private final ChangeData.Factory changeDataFactory; private final ChangeData.Factory changeDataFactory;
private final ChangeMessagesUtil cmUtil; private final ChangeMessagesUtil cmUtil;
private final ChangeControl.GenericFactory changeControlFactory;
private final ChangeNotes.Factory changeNotesFactory; private final ChangeNotes.Factory changeNotesFactory;
private final Provider<MergeOp> mergeOpProvider; private final Provider<MergeOp> mergeOpProvider;
private final MergeSuperSet mergeSuperSet; private final MergeSuperSet mergeSuperSet;
@@ -137,7 +134,6 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
GitRepositoryManager repoManager, GitRepositoryManager repoManager,
ChangeData.Factory changeDataFactory, ChangeData.Factory changeDataFactory,
ChangeMessagesUtil cmUtil, ChangeMessagesUtil cmUtil,
ChangeControl.GenericFactory changeControlFactory,
ChangeNotes.Factory changeNotesFactory, ChangeNotes.Factory changeNotesFactory,
Provider<MergeOp> mergeOpProvider, Provider<MergeOp> mergeOpProvider,
MergeSuperSet mergeSuperSet, MergeSuperSet mergeSuperSet,
@@ -149,7 +145,6 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
this.repoManager = repoManager; this.repoManager = repoManager;
this.changeDataFactory = changeDataFactory; this.changeDataFactory = changeDataFactory;
this.cmUtil = cmUtil; this.cmUtil = cmUtil;
this.changeControlFactory = changeControlFactory;
this.changeNotesFactory = changeNotesFactory; this.changeNotesFactory = changeNotesFactory;
this.mergeOpProvider = mergeOpProvider; this.mergeOpProvider = mergeOpProvider;
this.mergeSuperSet = mergeSuperSet; this.mergeSuperSet = mergeSuperSet;
@@ -233,19 +228,16 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
/** /**
* @param cs set of changes to be submitted at once * @param cs set of changes to be submitted at once
* @param project the name of the project
* @param identifiedUser the user who is checking to submit * @param identifiedUser the user who is checking to submit
* @return a reason why any of the changes is not submittable or null * @return a reason why any of the changes is not submittable or null
*/ */
private String problemsForSubmittingChangeset(ChangeSet cs, private String problemsForSubmittingChangeset(ChangeSet cs,
Project.NameKey project, IdentifiedUser identifiedUser) { IdentifiedUser identifiedUser) {
try { try {
@SuppressWarnings("resource") @SuppressWarnings("resource")
ReviewDb db = dbProvider.get(); ReviewDb db = dbProvider.get();
for (PatchSet.Id psId : cs.patchIds()) { for (ChangeData c : cs.changes()) {
ChangeControl changeControl = changeControlFactory ChangeControl changeControl = c.changeControl(identifiedUser);
.controlFor(db, project, psId.getParentKey(), identifiedUser);
ChangeData c = changeDataFactory.create(db, changeControl);
if (!changeControl.isVisible(db)) { if (!changeControl.isVisible(db)) {
return BLOCKED_HIDDEN_SUBMIT_TOOLTIP; return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
@@ -270,7 +262,7 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
} }
} catch (ResourceConflictException e) { } catch (ResourceConflictException e) {
return BLOCKED_SUBMIT_TOOLTIP; return BLOCKED_SUBMIT_TOOLTIP;
} catch (NoSuchChangeException | OrmException e) { } catch (OrmException e) {
log.error("Error checking if change is submittable", e); log.error("Error checking if change is submittable", e);
throw new OrmRuntimeException("Could not determine problems for the change", e); throw new OrmRuntimeException("Could not determine problems for the change", e);
} }
@@ -329,7 +321,8 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
ChangeSet cs; ChangeSet cs;
try { try {
cs = mergeSuperSet.completeChangeSet(db, cd.change()); cs = mergeSuperSet.completeChangeSet(
db, cd.change(), resource.getControl().getUser());
} catch (OrmException | IOException e) { } catch (OrmException | IOException e) {
throw new OrmRuntimeException("Could not determine complete set of " + throw new OrmRuntimeException("Could not determine complete set of " +
"changes to be submitted", e); "changes to be submitted", e);
@@ -343,8 +336,8 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
&& !Strings.isNullOrEmpty(topic) && !Strings.isNullOrEmpty(topic)
&& topicSize > 1; && topicSize > 1;
String submitProblems = problemsForSubmittingChangeset(cs, String submitProblems =
resource.getProject(), resource.getUser()); problemsForSubmittingChangeset(cs, resource.getUser());
if (submitProblems != null) { if (submitProblems != null) {
return new UiAction.Description() return new UiAction.Description()
.setLabel(treatWithTopic .setLabel(treatWithTopic

View File

@@ -23,6 +23,7 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.WalkSorter.PatchSetData; import com.google.gerrit.server.change.WalkSorter.PatchSetData;
import com.google.gerrit.server.git.ChangeSet; import com.google.gerrit.server.git.ChangeSet;
import com.google.gerrit.server.git.MergeSuperSet; import com.google.gerrit.server.git.MergeSuperSet;
@@ -74,7 +75,7 @@ public class SubmittedTogether implements RestReadView<ChangeResource> {
Change c = resource.getChange(); Change c = resource.getChange();
List<ChangeData> cds; List<ChangeData> cds;
if (c.getStatus().isOpen()) { if (c.getStatus().isOpen()) {
cds = getForOpenChange(c); cds = getForOpenChange(c, resource.getControl().getUser());
} else if (c.getStatus().asChangeStatus() == ChangeStatus.MERGED) { } else if (c.getStatus().asChangeStatus() == ChangeStatus.MERGED) {
cds = getForMergedChange(c); cds = getForMergedChange(c);
} else { } else {
@@ -99,9 +100,9 @@ public class SubmittedTogether implements RestReadView<ChangeResource> {
} }
} }
private List<ChangeData> getForOpenChange(Change c) private List<ChangeData> getForOpenChange(Change c, CurrentUser user)
throws OrmException, IOException { throws OrmException, IOException {
ChangeSet cs = mergeSuperSet.completeChangeSet(dbProvider.get(), c); ChangeSet cs = mergeSuperSet.completeChangeSet(dbProvider.get(), c, user);
return cs.changes().asList(); return cs.changes().asList();
} }

View File

@@ -547,7 +547,7 @@ public class MergeOp implements AutoCloseable {
this.db = db; this.db = db;
logDebug("Beginning integration of {}", change); logDebug("Beginning integration of {}", change);
try { try {
ChangeSet cs = mergeSuperSet.completeChangeSet(db, change); ChangeSet cs = mergeSuperSet.completeChangeSet(db, change, caller);
checkState(cs.ids().contains(change.getId()), checkState(cs.ids().contains(change.getId()),
"change %s missing from %s", change.getId(), cs); "change %s missing from %s", change.getId(), cs);
this.commits = new CommitStatus(cs); this.commits = new CommitStatus(cs);

View File

@@ -24,6 +24,7 @@ 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.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.server.CurrentUser;
import com.google.gerrit.server.change.Submit; import com.google.gerrit.server.change.Submit;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.ChangeField; import com.google.gerrit.server.index.ChangeField;
@@ -90,21 +91,22 @@ public class MergeSuperSet {
this.repoManager = repoManager; this.repoManager = repoManager;
} }
public ChangeSet completeChangeSet(ReviewDb db, Change change) public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
throws MissingObjectException, IncorrectObjectTypeException, IOException, throws MissingObjectException, IncorrectObjectTypeException, IOException,
OrmException { OrmException {
ChangeData cd = ChangeData cd =
changeDataFactory.create(db, change.getProject(), change.getId()); changeDataFactory.create(db, change.getProject(), change.getId());
cd.changeControl(user);
if (Submit.wholeTopicEnabled(cfg)) { if (Submit.wholeTopicEnabled(cfg)) {
return completeChangeSetIncludingTopics(db, new ChangeSet(cd)); return completeChangeSetIncludingTopics(db, new ChangeSet(cd), user);
} else { } else {
return completeChangeSetWithoutTopic(db, new ChangeSet(cd)); return completeChangeSetWithoutTopic(db, new ChangeSet(cd), user);
} }
} }
private ChangeSet completeChangeSetWithoutTopic(ReviewDb db, ChangeSet changes) private ChangeSet completeChangeSetWithoutTopic(ReviewDb db, ChangeSet changes,
throws MissingObjectException, IncorrectObjectTypeException, IOException, CurrentUser user) throws MissingObjectException,
OrmException { IncorrectObjectTypeException, IOException, OrmException {
List<ChangeData> ret = new ArrayList<>(); List<ChangeData> ret = new ArrayList<>();
Multimap<Project.NameKey, Change.Id> pc = changes.changesByProject(); Multimap<Project.NameKey, Change.Id> pc = changes.changesByProject();
@@ -113,6 +115,7 @@ public class MergeSuperSet {
RevWalk rw = CodeReviewCommit.newRevWalk(repo)) { RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
for (Change.Id cId : pc.get(project)) { for (Change.Id cId : pc.get(project)) {
ChangeData cd = changeDataFactory.create(db, project, cId); ChangeData cd = changeDataFactory.create(db, project, cId);
cd.changeControl(user);
SubmitTypeRecord str = cd.submitTypeRecord(); SubmitTypeRecord str = cd.submitTypeRecord();
if (!str.isOk()) { if (!str.isOk()) {
@@ -169,11 +172,12 @@ public class MergeSuperSet {
} }
private ChangeSet completeChangeSetIncludingTopics( private ChangeSet completeChangeSetIncludingTopics(
ReviewDb db, ChangeSet changes) throws MissingObjectException, ReviewDb db, ChangeSet changes, CurrentUser user)
IncorrectObjectTypeException, IOException, OrmException { throws MissingObjectException, IncorrectObjectTypeException, IOException,
OrmException {
Set<String> topicsTraversed = new HashSet<>(); Set<String> topicsTraversed = new HashSet<>();
boolean done = false; boolean done = false;
ChangeSet newCs = completeChangeSetWithoutTopic(db, changes); ChangeSet newCs = completeChangeSetWithoutTopic(db, changes, user);
while (!done) { while (!done) {
List<ChangeData> chgs = new ArrayList<>(); List<ChangeData> chgs = new ArrayList<>();
done = true; done = true;
@@ -187,7 +191,7 @@ public class MergeSuperSet {
} }
} }
changes = new ChangeSet(chgs); changes = new ChangeSet(chgs);
newCs = completeChangeSetWithoutTopic(db, changes); newCs = completeChangeSetWithoutTopic(db, changes, user);
} }
return newCs; return newCs;
} }

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.notedb; package com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST; import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
@@ -174,6 +175,10 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
public ChangeNotes create(ReviewDb db, Project.NameKey project, public ChangeNotes create(ReviewDb db, Project.NameKey project,
Change.Id changeId) throws OrmException { Change.Id changeId) throws OrmException {
Change change = db.changes().get(changeId); Change change = db.changes().get(changeId);
checkArgument(change.getProject().equals(project),
"passed project %s when creating ChangeNotes for %s, but actual"
+ " project is %s",
project, changeId, change.getProject());
// TODO: Throw NoSuchChangeException when the change is not found in the // TODO: Throw NoSuchChangeException when the change is not found in the
// database // database
return new ChangeNotes(repoManager, migration, allUsersProvider, project, return new ChangeNotes(repoManager, migration, allUsersProvider, project,

View File

@@ -618,6 +618,13 @@ public class ChangeData {
public ChangeControl changeControl(CurrentUser user) throws OrmException { public ChangeControl changeControl(CurrentUser user) throws OrmException {
if (changeControl != null) { if (changeControl != null) {
CurrentUser oldUser = user;
// TODO(dborowitz): This is a hack; general CurrentUser equality would be
// better.
if (user.isIdentifiedUser() && oldUser.isIdentifiedUser()
&& user.getAccountId().equals(oldUser.getAccountId())) {
return changeControl;
}
throw new IllegalStateException( throw new IllegalStateException(
"user already specified: " + changeControl.getUser()); "user already specified: " + changeControl.getUser());
} }