Bump Change.lastUpdatedOn when adding reviewers

The agreement in a recent discussion[1] was that we want to add
ChangeMessages when adding reviewers to a change. At that point, we
would bump the lasUpdatedOn field in Change, since we always do that
when adding ChangeMessages. Thus there is no real reason to *not* bump
lastUpdatedOn when adding a reviewer; go ahead and do that now. (This
change is just about lastUpdatedOn, and does not actually add that
message.)

This change removes the only place other than draft comments where we
modify a change but don't bump lastUpdatedOn. We still need to deal
with converting old ReviewDb changes to NoteDb where there might be
entities in the bundle that are newer than the lastUpdatedOn
timestamp. Don't worry about messing around with timestamps in NoteDb;
instead just use the latest timestamp from across the whole entity
group (excluding draft comments) when comparing to a ReviewDb
ChangeBundle. This should get us the same results in NoteDb from
reading just the change meta graph.

[1] https://groups.google.com/d/topic/repo-discuss/aULNkde4DKc/discussion

Change-Id: I51978a0234b9c70d9d84a16fc7d30477bf6e3596
This commit is contained in:
Dave Borowitz
2016-04-29 09:53:43 -04:00
parent f5dc67d45d
commit 1e20979bdb
6 changed files with 118 additions and 38 deletions

View File

@@ -26,6 +26,7 @@ import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS
import static com.google.gerrit.server.project.Util.blockLabel; import static com.google.gerrit.server.project.Util.blockLabel;
import static com.google.gerrit.server.project.Util.category; import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value; import static com.google.gerrit.server.project.Util.value;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
@@ -69,12 +70,15 @@ import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.Util; import com.google.gerrit.server.project.Util;
import com.google.gerrit.testutil.FakeEmailSender.Message; import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.gerrit.testutil.NoteDbMode; import com.google.gerrit.testutil.NoteDbMode;
import com.google.gerrit.testutil.TestTimeUtil;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.sql.Timestamp; import java.sql.Timestamp;
@@ -88,6 +92,18 @@ import java.util.Map;
@NoHttpd @NoHttpd
public class ChangeIT extends AbstractDaemonTest { public class ChangeIT extends AbstractDaemonTest {
private String systemTimeZone;
@Before
public void setTimeForTesting() {
systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
}
@After
public void resetTime() {
TestTimeUtil.useSystemTime();
System.setProperty("user.timezone", systemTimeZone);
}
@Test @Test
public void get() throws Exception { public void get() throws Exception {
@@ -503,6 +519,7 @@ public class ChangeIT extends AbstractDaemonTest {
@Test @Test
public void addReviewer() throws Exception { public void addReviewer() throws Exception {
TestTimeUtil.resetWithClockStep(1, SECONDS);
PushOneCommit.Result r = createChange(); PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r); ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag(); String oldETag = rsrc.getETag();
@@ -538,10 +555,10 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(reviewers.iterator().next()._accountId) assertThat(reviewers.iterator().next()._accountId)
.isEqualTo(user.getId().get()); .isEqualTo(user.getId().get());
// Ensure ETag is updated but lastUpdatedOn isn't. // Ensure ETag and lastUpdatedOn are updated.
rsrc = parseResource(r); rsrc = parseResource(r);
assertThat(rsrc.getETag()).isNotEqualTo(oldETag); assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
assertThat(rsrc.getChange().getLastUpdatedOn()).isEqualTo(oldTs); assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
} }
@Test @Test

View File

@@ -279,7 +279,6 @@ public class PostReviewers implements RestModifyView<ChangeResource, AddReviewer
return false; return false;
} }
patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes()); patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
ctx.bumpLastUpdatedOn(false);
return true; return true;
} }

View File

@@ -307,6 +307,27 @@ public class ChangeBundle {
return ImmutableList.copyOf(diffs); return ImmutableList.copyOf(diffs);
} }
private Timestamp getLatestTimestamp() {
Ordering<Timestamp> o = Ordering.natural();
Timestamp ts = change.getLastUpdatedOn();
for (ChangeMessage cm : getChangeMessages()) {
ts = o.max(ts, cm.getWrittenOn());
}
for (PatchSet ps : getPatchSets()) {
ts = o.max(ts, ps.getCreatedOn());
}
for (PatchSetApproval psa : getPatchSetApprovals()) {
ts = o.max(ts, psa.getGranted());
}
for (PatchLineComment plc : getPatchLineComments()) {
// Ignore draft comments, as they do not show up in the change meta graph.
if (plc.getStatus() != PatchLineComment.Status.DRAFT) {
ts = o.max(ts, plc.getWrittenOn());
}
}
return ts;
}
private static void diffChanges(List<String> diffs, ChangeBundle bundleA, private static void diffChanges(List<String> diffs, ChangeBundle bundleA,
ChangeBundle bundleB) { ChangeBundle bundleB) {
Change a = bundleA.change; Change a = bundleA.change;
@@ -315,19 +336,28 @@ public class ChangeBundle {
boolean excludeOrigSubj = false; boolean excludeOrigSubj = false;
boolean excludeTopic = false; boolean excludeTopic = false;
Timestamp aUpdated = a.getLastUpdatedOn();
Timestamp bUpdated = b.getLastUpdatedOn();
// Ignore null original subject on the ReviewDb side, as this field is // Ignore null original subject on the ReviewDb side, as this field is
// always set in NoteDb. // always set in NoteDb.
// //
// Ignore empty topic on the ReviewDb side if it is null on the NoteDb side. // Ignore empty topic on the ReviewDb side if it is null on the NoteDb side.
//
// Use max timestamp of all ReviewDb entities when comparing with NoteDb.
if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) { if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
excludeOrigSubj = a.getOriginalSubjectOrNull() == null; excludeOrigSubj = a.getOriginalSubjectOrNull() == null;
excludeTopic = "".equals(a.getTopic()) && b.getTopic() == null; excludeTopic = "".equals(a.getTopic()) && b.getTopic() == null;
aUpdated = bundleA.getLatestTimestamp();
} else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) { } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
excludeOrigSubj = b.getOriginalSubjectOrNull() == null; excludeOrigSubj = b.getOriginalSubjectOrNull() == null;
excludeTopic = a.getTopic() == null && "".equals(b.getTopic()); excludeTopic = a.getTopic() == null && "".equals(b.getTopic());
bUpdated = bundleB.getLatestTimestamp();
} }
List<String> exclude = Lists.newArrayList("rowVersion", "noteDbState"); String updatedField = "lastUpdatedOn";
List<String> exclude =
Lists.newArrayList(updatedField, "noteDbState", "rowVersion");
if (excludeOrigSubj) { if (excludeOrigSubj) {
exclude.add("originalSubject"); exclude.add("originalSubject");
} }
@@ -336,6 +366,15 @@ public class ChangeBundle {
} }
diffColumnsExcluding(diffs, Change.class, desc, bundleA, a, bundleB, b, diffColumnsExcluding(diffs, Change.class, desc, bundleA, a, bundleB, b,
exclude); exclude);
// Allow timestamps to either be exactly equal (within slop), or the NoteDb
// timestamp to be equal to the latest entity timestamp in the whole
// ReviewDb bundle (within slop).
if (timestampsDiffer(bundleA, a.getLastUpdatedOn(),
bundleB, b.getLastUpdatedOn())) {
diffTimestamps(diffs, desc, bundleA, aUpdated, bundleB, bUpdated,
"effective last updated time");
}
} }
private static void diffChangeMessages(List<String> diffs, private static void diffChangeMessages(List<String> diffs,
@@ -516,15 +555,28 @@ public class ChangeBundle {
| SecurityException e) { | SecurityException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
diffTimestamps(diffs, desc, bundleA, ta, bundleB, tb, field);
}
private static void diffTimestamps(List<String> diffs, String desc,
ChangeBundle bundleA, Timestamp ta, ChangeBundle bundleB, Timestamp tb,
String fieldDesc) {
if (bundleA.source == bundleB.source || ta == null || tb == null) { if (bundleA.source == bundleB.source || ta == null || tb == null) {
diffValues(diffs, desc, ta, tb, field); diffValues(diffs, desc, ta, tb, fieldDesc);
} else if (bundleA.source == NOTE_DB) { } else if (bundleA.source == NOTE_DB) {
diffTimestamps(diffs, desc, ta, tb, field); diffTimestamps(diffs, desc, ta, tb, fieldDesc);
} else { } else {
diffTimestamps(diffs, desc, tb, ta, field); diffTimestamps(diffs, desc, tb, ta, fieldDesc);
} }
} }
private static boolean timestampsDiffer(ChangeBundle bundleA, Timestamp ta,
ChangeBundle bundleB, Timestamp tb) {
List<String> tempDiffs = new ArrayList<>(1);
diffTimestamps(tempDiffs, "temp", bundleA, ta, bundleB, tb, "temp");
return !tempDiffs.isEmpty();
}
private static void diffTimestamps(List<String> diffs, String desc, private static void diffTimestamps(List<String> diffs, String desc,
Timestamp tsFromNoteDb, Timestamp tsFromReviewDb, String field) { Timestamp tsFromNoteDb, Timestamp tsFromReviewDb, String field) {
// Because ChangeRebuilder may batch events together that are several // Because ChangeRebuilder may batch events together that are several

View File

@@ -207,18 +207,14 @@ class ChangeNotesParser implements AutoCloseable {
Timestamp ts = Timestamp ts =
new Timestamp(commit.getCommitterIdent().getWhen().getTime()); new Timestamp(commit.getCommitterIdent().getWhen().getTime());
boolean updateTs = commit.getParentCount() == 0;
createdOn = ts; createdOn = ts;
parseTag(commit); parseTag(commit);
updateTs |= tag != null;
if (branch == null) { if (branch == null) {
branch = parseBranch(commit); branch = parseBranch(commit);
updateTs |= branch != null;
} }
if (status == null) { if (status == null) {
status = parseStatus(commit); status = parseStatus(commit);
updateTs |= status != null;
} }
PatchSet.Id psId = parsePatchSetId(commit); PatchSet.Id psId = parsePatchSetId(commit);
@@ -238,7 +234,6 @@ class ChangeNotesParser implements AutoCloseable {
if (changeId == null) { if (changeId == null) {
changeId = parseChangeId(commit); changeId = parseChangeId(commit);
updateTs |= changeId != null;
} }
String currSubject = parseSubject(commit); String currSubject = parseSubject(commit);
@@ -247,28 +242,22 @@ class ChangeNotesParser implements AutoCloseable {
subject = currSubject; subject = currSubject;
} }
originalSubject = currSubject; originalSubject = currSubject;
updateTs = true;
} }
updateTs |= parseChangeMessage(psId, accountId, commit, ts) != null; parseChangeMessage(psId, accountId, commit, ts);
if (topic == null) { if (topic == null) {
topic = parseTopic(commit); topic = parseTopic(commit);
updateTs |= topic != null;
} }
Set<String> oldHashtags = hashtags;
parseHashtags(commit); parseHashtags(commit);
updateTs |= hashtags != oldHashtags;
if (submissionId == null) { if (submissionId == null) {
submissionId = parseSubmissionId(commit); submissionId = parseSubmissionId(commit);
updateTs |= submissionId != null;
} }
ObjectId currRev = parseRevision(commit); ObjectId currRev = parseRevision(commit);
if (currRev != null) { if (currRev != null) {
parsePatchSet(psId, currRev, accountId, ts); parsePatchSet(psId, currRev, accountId, ts);
updateTs = true;
} }
parseGroups(psId, commit); parseGroups(psId, commit);
@@ -276,12 +265,10 @@ class ChangeNotesParser implements AutoCloseable {
// Only parse the most recent set of submit records; any older ones are // Only parse the most recent set of submit records; any older ones are
// still there, but not currently used. // still there, but not currently used.
parseSubmitRecords(commit.getFooterLineValues(FOOTER_SUBMITTED_WITH)); parseSubmitRecords(commit.getFooterLineValues(FOOTER_SUBMITTED_WITH));
updateTs |= !submitRecords.isEmpty();
} }
for (String line : commit.getFooterLineValues(FOOTER_LABEL)) { for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
parseApproval(psId, accountId, ts, line); parseApproval(psId, accountId, ts, line);
updateTs = true;
} }
for (ReviewerStateInternal state : ReviewerStateInternal.values()) { for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
@@ -292,10 +279,8 @@ class ChangeNotesParser implements AutoCloseable {
// behavior. // behavior.
} }
if (updateTs) { if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) { lastUpdatedOn = ts;
lastUpdatedOn = ts;
}
} }
} }
@@ -476,7 +461,7 @@ class ChangeNotesParser implements AutoCloseable {
throw invalidFooter(FOOTER_PATCH_SET, psIdLine); throw invalidFooter(FOOTER_PATCH_SET, psIdLine);
} }
private ChangeMessage parseChangeMessage(PatchSet.Id psId, private void parseChangeMessage(PatchSet.Id psId,
Account.Id accountId, ChangeNotesCommit commit, Timestamp ts) { Account.Id accountId, ChangeNotesCommit commit, Timestamp ts) {
byte[] raw = commit.getRawBuffer(); byte[] raw = commit.getRawBuffer();
int size = raw.length; int size = raw.length;
@@ -484,12 +469,12 @@ class ChangeNotesParser implements AutoCloseable {
int subjectStart = RawParseUtils.commitMessage(raw, 0); int subjectStart = RawParseUtils.commitMessage(raw, 0);
if (subjectStart < 0 || subjectStart >= size) { if (subjectStart < 0 || subjectStart >= size) {
return null; return;
} }
int subjectEnd = RawParseUtils.endOfParagraph(raw, subjectStart); int subjectEnd = RawParseUtils.endOfParagraph(raw, subjectStart);
if (subjectEnd == size) { if (subjectEnd == size) {
return null; return;
} }
int changeMessageStart; int changeMessageStart;
@@ -499,7 +484,7 @@ class ChangeNotesParser implements AutoCloseable {
} else if (raw[subjectEnd] == '\r') { } else if (raw[subjectEnd] == '\r') {
changeMessageStart = subjectEnd + 4; //\r\n\r\n ends paragraph changeMessageStart = subjectEnd + 4; //\r\n\r\n ends paragraph
} else { } else {
return null; return;
} }
int ptr = size - 1; int ptr = size - 1;
@@ -519,7 +504,7 @@ class ChangeNotesParser implements AutoCloseable {
} }
if (ptr <= changeMessageStart) { if (ptr <= changeMessageStart) {
return null; return;
} }
String changeMsgString = RawParseUtils.decode(enc, raw, String changeMsgString = RawParseUtils.decode(enc, raw,
@@ -533,7 +518,6 @@ class ChangeNotesParser implements AutoCloseable {
changeMessage.setTag(tag); changeMessage.setTag(tag);
changeMessagesByPatchSet.put(psId, changeMessage); changeMessagesByPatchSet.put(psId, changeMessage);
allChangeMessages.add(changeMessage); allChangeMessages.add(changeMessage);
return changeMessage;
} }
private void parseNotes() private void parseNotes()

View File

@@ -117,7 +117,7 @@ public class ChangeBundleTest {
"changeId differs for Changes: {" + id1 + "} != {" + id2 + "}", "changeId differs for Changes: {" + id1 + "} != {" + id2 + "}",
"createdOn differs for Changes:" "createdOn differs for Changes:"
+ " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}", + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}",
"lastUpdatedOn differs for Changes:" "effective last updated time differs for Changes:"
+ " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}"); + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}");
} }
@@ -155,7 +155,7 @@ public class ChangeBundleTest {
assertDiffs(b1, b2, assertDiffs(b1, b2,
"createdOn differs for Change.Id " + c1.getId() + ":" "createdOn differs for Change.Id " + c1.getId() + ":"
+ " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:02.0}", + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:02.0}",
"lastUpdatedOn differs for Change.Id " + c1.getId() + ":" "effective last updated time differs for Change.Id " + c1.getId() + ":"
+ " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:03.0}"); + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:03.0}");
// One NoteDb, slop is allowed. // One NoteDb, slop is allowed.
@@ -174,8 +174,8 @@ public class ChangeBundleTest {
comments(), NOTE_DB); comments(), NOTE_DB);
ChangeBundle b3 = new ChangeBundle(c3, messages(), patchSets(), approvals(), ChangeBundle b3 = new ChangeBundle(c3, messages(), patchSets(), approvals(),
comments(), REVIEW_DB); comments(), REVIEW_DB);
String msg = "lastUpdatedOn differs for Change.Id " + c1.getId() String msg = "effective last updated time differs for Change.Id "
+ " in NoteDb vs. ReviewDb:" + c1.getId() + " in NoteDb vs. ReviewDb:"
+ " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:10.0}"; + " {2009-09-30 17:00:01.0} != {2009-09-30 17:00:10.0}";
assertDiffs(b1, b3, msg); assertDiffs(b1, b3, msg);
assertDiffs(b3, b1, msg); assertDiffs(b3, b1, msg);
@@ -272,6 +272,35 @@ public class ChangeBundleTest {
+ " {topic} != {null}"); + " {topic} != {null}");
} }
@Test
public void diffChangesTakesMaxEntityTimestampFromReviewDb()
throws Exception {
Change c1 = TestChanges.newChange(
new Project.NameKey("project"), new Account.Id(100));
PatchSetApproval a = new PatchSetApproval(
new PatchSetApproval.Key(
c1.currentPatchSetId(), accountId, new LabelId("Code-Review")),
(short) 1,
TimeUtil.nowTs());
Change c2 = clone(c1);
c2.setLastUpdatedOn(a.getGranted());
// Both ReviewDb, exact match required.
ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(),
approvals(a), comments(), REVIEW_DB);
ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(),
approvals(a), comments(), REVIEW_DB);
assertDiffs(b1, b2,
"effective last updated time differs for Change.Id " + c1.getId() + ":"
+ " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}");
// NoteDb allows latest timestamp from all entities in bundle.
b2 = new ChangeBundle(c2, messages(), patchSets(),
approvals(a), comments(), NOTE_DB);
assertNoDiffs(b1, b2);
}
@Test @Test
public void diffChangeMessageKeySets() throws Exception { public void diffChangeMessageKeySets() throws Exception {
Change c = TestChanges.newChange(project, accountId); Change c = TestChanges.newChange(project, accountId);

View File

@@ -728,18 +728,17 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
Timestamp ts7 = newNotes(c).getChange().getLastUpdatedOn(); Timestamp ts7 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts7).isGreaterThan(ts6); assertThat(ts7).isGreaterThan(ts6);
// Updates that should not touch the timestamp.
update = newUpdate(c, changeOwner); update = newUpdate(c, changeOwner);
update.putReviewer(otherUser.getAccountId(), ReviewerStateInternal.REVIEWER); update.putReviewer(otherUser.getAccountId(), ReviewerStateInternal.REVIEWER);
update.commit(); update.commit();
Timestamp ts8 = newNotes(c).getChange().getLastUpdatedOn(); Timestamp ts8 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts8).isEqualTo(ts7); assertThat(ts8).isGreaterThan(ts7);
update = newUpdate(c, changeOwner); update = newUpdate(c, changeOwner);
update.setGroups(ImmutableList.of("a", "b")); update.setGroups(ImmutableList.of("a", "b"));
update.commit(); update.commit();
Timestamp ts9 = newNotes(c).getChange().getLastUpdatedOn(); Timestamp ts9 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts9).isEqualTo(ts8); assertThat(ts9).isGreaterThan(ts8);
// Finish off by merging the change. // Finish off by merging the change.
update = newUpdate(c, changeOwner); update = newUpdate(c, changeOwner);