Notedb: Implement patch set groups

This field is mutable, which adds the complication that we might
parse a new value for groups in a later commit, before parsing the
Commit field that allows us to populate the patch set maps. Use a
sentinel RevId to handle this case internally.

Change-Id: I4d168f078241660c8e72ff709655081d99ac2b66
This commit is contained in:
Dave Borowitz 2016-01-22 16:32:59 -05:00
parent 0a8ee423b2
commit 95437f623f
9 changed files with 169 additions and 35 deletions

View File

@ -47,6 +47,7 @@ import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.config.AllProjectsName;
@ -170,6 +171,9 @@ public abstract class AbstractDaemonTest {
@Inject
protected ChangeData.Factory changeDataFactory;
@Inject
protected PatchSetUtil psUtil;
protected TestRepository<InMemoryRepository> testRepo;
protected GerritServer server;
protected TestAccount admin;
@ -634,4 +638,8 @@ public abstract class AbstractDaemonTest {
protected PatchSet getPatchSet(PatchSet.Id psId) throws OrmException {
return changeDataFactory.create(db, psId.getParentKey()).patchSet(psId);
}
protected IdentifiedUser user(TestAccount testAccount) {
return identifiedUserFactory.create(Providers.of(db), testAccount.getId());
}
}

View File

@ -22,6 +22,7 @@ import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestSession;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
@ -29,6 +30,8 @@ import com.google.gerrit.server.change.GetRelated.ChangeAndCommit;
import com.google.gerrit.server.change.GetRelated.RelatedInfo;
import com.google.gerrit.server.edit.ChangeEditModifier;
import com.google.gerrit.server.edit.ChangeEditUtil;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@ -47,6 +50,9 @@ public class GetRelatedIT extends AbstractDaemonTest {
@Inject
private ChangeEditModifier editModifier;
@Inject
private BatchUpdate.Factory updateFactory;
@Test
public void getRelatedNoResult() throws Exception {
PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
@ -566,9 +572,7 @@ public class GetRelatedIT extends AbstractDaemonTest {
}
// Pretend PS1,1 was pushed before the groups field was added.
PatchSet ps1_1 = getPatchSet(psId1_1);
ps1_1.setGroups(null);
db.patchSets().update(ImmutableList.of(ps1_1));
setGroups(psId1_1, null);
indexer.index(changeDataFactory.create(db, psId1_1.getParentKey()));
// PS1,1 has no groups, so disappeared from related changes.
@ -625,6 +629,22 @@ public class GetRelatedIT extends AbstractDaemonTest {
return result;
}
private void setGroups(final PatchSet.Id psId,
final Iterable<String> groups) throws Exception {
try (BatchUpdate bu = updateFactory.create(
db, project, user(user), TimeUtil.nowTs())) {
bu.addOp(psId.getParentKey(), new BatchUpdate.Op() {
@Override
public boolean updateChange(ChangeContext ctx) throws OrmException {
PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps, groups);
return true;
}
});
bu.execute();
}
}
private void assertRelated(PatchSet.Id psId, ChangeAndCommit... expected)
throws Exception {
List<ChangeAndCommit> actual = getRelated(psId);

View File

@ -118,4 +118,11 @@ public class PatchSetUtil {
update.setPatchSetId(psId);
}
}
public void setGroups(ReviewDb db, ChangeUpdate update, PatchSet ps,
Iterable<String> groups) throws OrmException {
ps.setGroups(groups);
update.setGroups(groups);
db.patchSets().update(Collections.singleton(ps));
}
}

View File

@ -2423,29 +2423,27 @@ public class ReceiveCommits {
}
private void updateGroups(RequestState state)
throws OrmException, IOException {
ReviewDb db = state.db;
PatchSet ps = db.patchSets().atomicUpdate(psId,
new AtomicUpdate<PatchSet>() {
@Override
public PatchSet update(PatchSet ps) {
List<String> oldGroups = ps.getGroups();
if (oldGroups == null) {
if (groups == null) {
return null;
}
} else if (Sets.newHashSet(oldGroups).equals(groups)) {
return null;
throws RestApiException, UpdateException {
try (ObjectInserter oi = repo.newObjectInserter();
BatchUpdate bu = batchUpdateFactory.create(state.db,
magicBranch.dest.getParentKey(), user, TimeUtil.nowTs())) {
bu.addOp(psId.getParentKey(), new BatchUpdate.Op() {
@Override
public boolean updateChange(ChangeContext ctx) throws OrmException {
PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
List<String> oldGroups = ps.getGroups();
if (oldGroups == null) {
if (groups == null) {
return false;
}
ps.setGroups(groups);
return ps;
} else if (Sets.newHashSet(oldGroups).equals(groups)) {
return false;
}
});
if (ps != null) {
Change change = db.changes().get(psId.getParentKey());
if (change != null) {
indexer.index(db, change);
}
psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps, groups);
return true;
}
});
bu.execute();
}
}
@ -2454,7 +2452,7 @@ public class ReceiveCommits {
ListenableFuture<Void> future = changeUpdateExector.submit(
requestScopePropagator.wrap(new Callable<Void>() {
@Override
public Void call() throws OrmException, IOException {
public Void call() throws Exception {
try (RequestState state = requestState(caller)) {
updateGroups(state);
}

View File

@ -30,6 +30,7 @@ public class ChangeNoteUtil {
static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
static final FooterKey FOOTER_GROUPS = new FooterKey("Groups");
static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
static final FooterKey FOOTER_LABEL = new FooterKey("Label");
static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");

View File

@ -16,6 +16,7 @@ package com.google.gerrit.server.notedb;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
@ -84,6 +85,11 @@ import java.util.Set;
import java.util.TreeMap;
class ChangeNotesParser implements AutoCloseable {
// Sentinel RevId indicating a mutable field on a patch set was parsed, but
// the parser does not yet know its commit SHA-1.
private static final RevId PARTIAL_PATCH_SET =
new RevId("INVALID PARTIAL PATCH SET");
final Map<Account.Id, ReviewerStateInternal> reviewers;
final List<Account.Id> allPastReviewers;
final List<SubmitRecord> submitRecords;
@ -226,6 +232,7 @@ class ChangeNotesParser implements AutoCloseable {
if (currRev != null) {
parsePatchSet(psId, currRev, accountId, ts);
}
parseGroups(psId, commit);
if (submitRecords.isEmpty()) {
// Only parse the most recent set of submit records; any older ones are
@ -299,18 +306,36 @@ class ChangeNotesParser implements AutoCloseable {
private void parsePatchSet(PatchSet.Id psId, ObjectId rev,
Account.Id accountId, Timestamp ts) throws ConfigInvalidException {
if (patchSets.containsKey(psId)) {
PatchSet ps = patchSets.get(psId);
if (ps == null) {
ps = new PatchSet(psId);
patchSets.put(psId, ps);
} else if (ps.getRevision() != PARTIAL_PATCH_SET) {
throw new ConfigInvalidException(
String.format(
"Multiple revisions parsed for patch set %s: %s and %s",
psId.get(), patchSets.get(psId).getRevision(), rev.name()));
} else {
PatchSet ps = new PatchSet(psId);
ps.setRevision(new RevId(rev.name()));
ps.setUploader(accountId);
ps.setCreatedOn(ts);
patchSets.put(psId, ps);
}
ps.setRevision(new RevId(rev.name()));
ps.setUploader(accountId);
ps.setCreatedOn(ts);
}
private void parseGroups(PatchSet.Id psId, RevCommit commit)
throws ConfigInvalidException {
String groupsStr = parseOneFooter(commit, FOOTER_GROUPS);
if (groupsStr == null) {
return;
}
PatchSet ps = patchSets.get(psId);
if (ps == null) {
ps = new PatchSet(psId);
ps.setRevision(PARTIAL_PATCH_SET);
patchSets.put(psId, ps);
} else if (ps.getGroups() != null) {
return;
}
ps.setGroups(PatchSet.splitGroups(groupsStr));
}
private void parseHashtags(RevCommit commit) throws ConfigInvalidException {
@ -634,7 +659,14 @@ class ChangeNotesParser implements AutoCloseable {
}
}
private void updatePatchSetStates() {
private void updatePatchSetStates() throws ConfigInvalidException {
for (PatchSet ps : patchSets.values()) {
if (ps.getRevision() == PARTIAL_PATCH_SET) {
throw parseException("No %s found for patch set %s",
FOOTER_COMMIT, ps.getPatchSetId());
}
}
Set<PatchSet.Id> deleted =
Sets.newHashSetWithExpectedSize(patchSetStates.size());
for (Map.Entry<PatchSet.Id, PatchSetState> e : patchSetStates.entrySet()) {

View File

@ -17,6 +17,7 @@ package com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
@ -112,6 +113,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
private String changeMessage;
private ChangeNotes notes;
private PatchSetState psState;
private Iterable<String> groups;
private final ChangeDraftUpdate.Factory draftUpdateFactory;
private ChangeDraftUpdate draftUpdate;
@ -390,6 +392,10 @@ public class ChangeUpdate extends AbstractChangeUpdate {
this.psState = psState;
}
public void setGroups(Iterable<String> groups) {
this.groups = groups;
}
/** @return the tree id for the updated tree */
private ObjectId storeCommentsInNotes() throws OrmException, IOException {
ChangeNotes notes = ctl.getNotes().load();
@ -495,8 +501,13 @@ public class ChangeUpdate extends AbstractChangeUpdate {
addFooter(msg, FOOTER_COMMIT, commit.name());
}
Joiner comma = Joiner.on(',');
if (hashtags != null) {
addFooter(msg, FOOTER_HASHTAGS, Joiner.on(",").join(hashtags));
addFooter(msg, FOOTER_HASHTAGS, comma.join(hashtags));
}
if (groups != null) {
addFooter(msg, FOOTER_GROUPS, comma.join(groups));
}
for (Map.Entry<Account.Id, ReviewerStateInternal> e : reviewers.entrySet()) {
@ -579,7 +590,8 @@ public class ChangeUpdate extends AbstractChangeUpdate {
&& hashtags == null
&& topic == null
&& commit == null
&& psState == null;
&& psState == null
&& groups == null;
}
private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {

View File

@ -319,6 +319,32 @@ public class ChangeNotesParserTest extends AbstractChangeNotesTest {
+ "Subject: Some subject of a change\n");
}
@Test
public void parsePatchSetGroups() throws Exception {
assertParseSucceeds("Update change\n"
+ "\n"
+ "Patch-set: 1\n"
+ "Branch: refs/heads/master\n"
+ "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+ "Subject: Change subject\n"
+ "Groups: a,b,c\n");
// No patch set commit parsed on which we can set groups.
assertParseFails("Update change\n"
+ "\n"
+ "Patch-set: 1\n"
+ "Branch: refs/heads/master\n"
+ "Subject: Change subject\n"
+ "Groups: a,b,c\n");
assertParseFails("Update change\n"
+ "\n"
+ "Patch-set: 1\n"
+ "Branch: refs/heads/master\n"
+ "Commit: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+ "Subject: Change subject\n"
+ "Groups: a,b,c\n"
+ "Groups: d,e,f\n");
}
private RevCommit writeCommit(String body) throws Exception {
return writeCommit(body, ChangeNoteUtil.newIdent(
changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent,

View File

@ -658,6 +658,36 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
assertThat(notes.getComments()).isEmpty();
}
@Test
public void patchSetGroups() throws Exception {
Change c = newChange();
PatchSet.Id psId1 = c.currentPatchSetId();
ChangeNotes notes = newNotes(c);
assertThat(notes.getPatchSets().get(psId1).getGroups()).isNull();
// ps1
ChangeUpdate update = newUpdate(c, changeOwner);
update.setGroups(ImmutableList.of("a", "b"));
update.commit();
notes = newNotes(c);
assertThat(notes.getPatchSets().get(psId1).getGroups())
.containsExactly("a", "b").inOrder();
// ps2
incrementPatchSet(c);
PatchSet.Id psId2 = c.currentPatchSetId();
update = newUpdate(c, changeOwner);
update.setCommit(rw, tr.commit().message("PS2").create());
update.setGroups(ImmutableList.of("d"));
update.commit();
notes = newNotes(c);
assertThat(notes.getPatchSets().get(psId2).getGroups())
.containsExactly("d");
assertThat(notes.getPatchSets().get(psId1).getGroups())
.containsExactly("a", "b").inOrder();
}
@Test
public void emptyExceptSubject() throws Exception {
ChangeUpdate update = newUpdate(newChange(), changeOwner);