Merge "Merge branch 'stable-2.13'"
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 22a1f5d..50cef79 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -47,6 +47,7 @@
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -215,9 +216,22 @@
setApiUser(user);
ReviewInput in = new ReviewInput();
- in.label("Code-Review", 0);
+ in.label("Code-Review", 1);
in.message = "Still LGTM";
revision(r).review(in);
+
+ ApprovalInfo cr =
+ gApi.changes()
+ .id(changeId)
+ .get(EnumSet.of(ListChangesOption.DETAILED_LABELS))
+ .labels
+ .get("Code-Review")
+ .all
+ .stream()
+ .filter(a -> a._accountId == user.getId().get())
+ .findFirst()
+ .get();
+ assertThat(cr.postSubmit).isTrue();
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index c62839e..7db5b9e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -285,7 +285,7 @@
+ "Gerrit-MessageType: comment\n"
+ "Gerrit-Comment-Date: "
+ timestamp
- + "\n";
+ + " \n";
}
private MailMessage.Builder messageBuilderWithDefaultFields() {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index 04080af..3d2c3d40 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -790,7 +790,7 @@
PushOneCommit.FILE_NAME,
"new contents",
r.getChangeId())
- .to("refs/heads/master");
+ .to("refs/for/master");
r.assertOkStatus();
PatchSet.Id psId = r.getPatchSetId();
@@ -1216,6 +1216,64 @@
rebuilderWrapper.rebuild(db, id);
}
+ @Test
+ public void commitWithCrLineEndings() throws Exception {
+ PushOneCommit.Result r =
+ createChange("Subject\r\rBody\r", PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
+ Change c = r.getChange().change();
+
+ // This assertion demonstrates an arguable bug in JGit's commit subject
+ // parsing, and shows how this kind of data might have gotten into
+ // ReviewDb. If that bug ever gets fixed upstream, this assert may start
+ // failing. If that happens, this test can be rewritten to directly set the
+ // subject field in ReviewDb.
+ assertThat(c.getSubject()).isEqualTo("Subject\r\rBody");
+
+ checker.rebuildAndCheckChanges(c.getId());
+ }
+
+ @Test
+ public void patchSetsOutOfOrder() throws Exception {
+ String id = createChange().getChangeId();
+ amendChange(id);
+ PushOneCommit.Result r = amendChange(id);
+
+ ChangeData cd = r.getChange();
+ PatchSet.Id psId3 = cd.change().currentPatchSetId();
+ assertThat(psId3.get()).isEqualTo(3);
+
+ PatchSet ps1 = db.patchSets().get(new PatchSet.Id(cd.getId(), 1));
+ PatchSet ps3 = db.patchSets().get(psId3);
+ assertThat(ps1.getCreatedOn()).isLessThan(ps3.getCreatedOn());
+
+ // Simulate an old Gerrit bug by setting the created timestamp of the latest
+ // patch set ID to the timestamp of PS1.
+ ps3.setCreatedOn(ps1.getCreatedOn());
+ db.patchSets().update(Collections.singleton(ps3));
+
+ checker.rebuildAndCheckChanges(cd.getId());
+
+ setNotesMigration(true, true);
+ cd = changeDataFactory.create(db, project, cd.getId());
+ assertThat(cd.change().currentPatchSetId()).isEqualTo(psId3);
+
+ List<PatchSet> patchSets = ImmutableList.copyOf(cd.patchSets());
+ assertThat(patchSets).hasSize(3);
+
+ PatchSet newPs1 = patchSets.get(0);
+ assertThat(newPs1.getId()).isEqualTo(ps1.getId());
+ assertThat(newPs1.getCreatedOn()).isEqualTo(ps1.getCreatedOn());
+
+ PatchSet newPs2 = patchSets.get(1);
+ assertThat(newPs2.getCreatedOn()).isGreaterThan(newPs1.getCreatedOn());
+
+ PatchSet newPs3 = patchSets.get(2);
+ assertThat(newPs3.getId()).isEqualTo(ps3.getId());
+ // Migrated with a newer timestamp than the original, to preserve ordering.
+ assertThat(newPs3.getCreatedOn()).isAtLeast(newPs2.getCreatedOn());
+ assertThat(newPs3.getCreatedOn()).isGreaterThan(ps1.getCreatedOn());
+ }
+
private void assertChangesReadOnly(RestApiException e) throws Exception {
Throwable cause = e.getCause();
assertThat(cause).isInstanceOf(UpdateException.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java
index c812568..f703ed7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java
@@ -23,9 +23,14 @@
import com.google.gerrit.server.mail.MetadataName;
import java.sql.Timestamp;
import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/** Parse metadata from inbound email */
public class MetadataParser {
+ private static final Logger log = LoggerFactory.getLogger(MailProcessor.class.getName());
+
public static MailMetadata parse(MailMessage m) {
MailMetadata metadata = new MailMetadata();
// Find author
@@ -40,8 +45,12 @@
String ps = header.substring(toHeaderWithDelimiter(MetadataName.PATCH_SET).length());
metadata.patchSet = Ints.tryParse(ps);
} else if (header.startsWith(toHeaderWithDelimiter(MetadataName.TIMESTAMP))) {
- String ts = header.substring(toHeaderWithDelimiter(MetadataName.TIMESTAMP).length());
- metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
+ String ts = header.substring(toHeaderWithDelimiter(MetadataName.TIMESTAMP).length()).trim();
+ try {
+ metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
+ } catch (DateTimeParseException e) {
+ log.error("Mail: Error while parsing timestamp from header of message " + m.id(), e);
+ }
} else if (header.startsWith(toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE))) {
metadata.messageType =
header.substring(toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE).length());
@@ -53,18 +62,18 @@
// If the required fields were not yet found, continue to parse the text
if (!Strings.isNullOrEmpty(m.textContent())) {
- String[] lines = m.textContent().split("\n");
- extractFooters(lines, metadata);
+ String[] lines = m.textContent().replace("\r\n", "\n").split("\n");
+ extractFooters(lines, metadata, m);
if (metadata.hasRequiredFields()) {
return metadata;
}
}
// If the required fields were not yet found, continue to parse the HTML
- // HTML footer are contained inside a <p> tag
+ // HTML footer are contained inside a <div> tag
if (!Strings.isNullOrEmpty(m.htmlContent())) {
- String[] lines = m.htmlContent().split("</p>");
- extractFooters(lines, metadata);
+ String[] lines = m.htmlContent().replace("\r\n", "\n").split("</div>");
+ extractFooters(lines, metadata, m);
if (metadata.hasRequiredFields()) {
return metadata;
}
@@ -73,7 +82,7 @@
return metadata;
}
- private static void extractFooters(String[] lines, MailMetadata metadata) {
+ private static void extractFooters(String[] lines, MailMetadata metadata, MailMessage m) {
for (String line : lines) {
if (metadata.changeId == null && line.contains(MetadataName.CHANGE_ID)) {
metadata.changeId = extractFooter(toFooterWithDelimiter(MetadataName.CHANGE_ID), line);
@@ -82,7 +91,11 @@
Ints.tryParse(extractFooter(toFooterWithDelimiter(MetadataName.PATCH_SET), line));
} else if (metadata.timestamp == null && line.contains(MetadataName.TIMESTAMP)) {
String ts = extractFooter(toFooterWithDelimiter(MetadataName.TIMESTAMP), line);
- metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
+ try {
+ metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
+ } catch (DateTimeParseException e) {
+ log.error("Mail: Error while parsing timestamp from footer of message " + m.id(), e);
+ }
} else if (metadata.messageType == null && line.contains(MetadataName.MESSAGE_TYPE)) {
metadata.messageType =
extractFooter(toFooterWithDelimiter(MetadataName.MESSAGE_TYPE), line);
@@ -91,6 +104,6 @@
}
private static String extractFooter(String key, String line) {
- return line.substring(line.indexOf(key) + key.length(), line.length());
+ return line.substring(line.indexOf(key) + key.length(), line.length()).trim();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
index eaa0335..d5b1b3d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -22,6 +22,7 @@
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
+import static java.util.stream.Collectors.toList;
import com.google.auto.value.AutoValue;
import com.google.common.base.CharMatcher;
@@ -50,6 +51,7 @@
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
@@ -434,21 +436,23 @@
excludeCreatedOn =
!timestampsDiffer(bundleA, bundleA.getFirstPatchSetTime(), bundleB, b.getCreatedOn());
aSubj = cleanReviewDbSubject(aSubj);
+ bSubj = cleanNoteDbSubject(bSubj);
excludeCurrentPatchSetId = !bundleA.validPatchSetPredicate().apply(a.currentPatchSetId());
excludeSubject = bSubj.startsWith(aSubj) || excludeCurrentPatchSetId;
excludeOrigSubj = true;
- String aTopic = trimLeadingOrNull(a.getTopic());
+ String aTopic = trimOrNull(a.getTopic());
excludeTopic =
Objects.equals(aTopic, b.getTopic()) || "".equals(aTopic) && b.getTopic() == null;
aUpdated = bundleA.getLatestTimestamp();
} else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
excludeCreatedOn =
!timestampsDiffer(bundleA, a.getCreatedOn(), bundleB, bundleB.getFirstPatchSetTime());
+ aSubj = cleanNoteDbSubject(aSubj);
bSubj = cleanReviewDbSubject(bSubj);
excludeCurrentPatchSetId = !bundleB.validPatchSetPredicate().apply(b.currentPatchSetId());
excludeSubject = aSubj.startsWith(bSubj) || excludeCurrentPatchSetId;
excludeOrigSubj = true;
- String bTopic = trimLeadingOrNull(b.getTopic());
+ String bTopic = trimOrNull(b.getTopic());
excludeTopic =
Objects.equals(bTopic, a.getTopic()) || a.getTopic() == null && "".equals(bTopic);
bUpdated = bundleB.getLatestTimestamp();
@@ -484,8 +488,8 @@
}
}
- private static String trimLeadingOrNull(String s) {
- return s != null ? CharMatcher.whitespace().trimLeadingFrom(s) : null;
+ private static String trimOrNull(String s) {
+ return s != null ? CharMatcher.whitespace().trimFrom(s) : null;
}
private static String cleanReviewDbSubject(String s) {
@@ -501,7 +505,11 @@
if (rn >= 0) {
s = s.substring(0, rn);
}
- return s;
+ return ChangeNoteUtil.sanitizeFooter(s);
+ }
+
+ private static String cleanNoteDbSubject(String s) {
+ return ChangeNoteUtil.sanitizeFooter(s);
}
/**
@@ -642,12 +650,47 @@
List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
Map<PatchSet.Id, PatchSet> as = bundleA.patchSets;
Map<PatchSet.Id, PatchSet> bs = bundleB.patchSets;
- for (PatchSet.Id id : diffKeySets(diffs, as, bs)) {
+ Set<PatchSet.Id> ids = diffKeySets(diffs, as, bs);
+
+ // Old versions of Gerrit had a bug that created patch sets during
+ // rebase or submission with a createdOn timestamp earlier than the patch
+ // set it was replacing. (In the cases I examined, it was equal to createdOn
+ // for the change, but we're not counting on this exact behavior.)
+ //
+ // ChangeRebuilder ensures patch set events come out in order, but it's hard
+ // to predict what the resulting timestamps would look like. So, completely
+ // ignore the createdOn timestamps if both:
+ // * ReviewDb timestamps are non-monotonic.
+ // * NoteDb timestamps are monotonic.
+ boolean excludeCreatedOn = false;
+ if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
+ excludeCreatedOn = !createdOnIsMonotonic(as, ids) && createdOnIsMonotonic(bs, ids);
+ } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
+ excludeCreatedOn = createdOnIsMonotonic(as, ids) && !createdOnIsMonotonic(bs, ids);
+ }
+
+ for (PatchSet.Id id : ids) {
PatchSet a = as.get(id);
PatchSet b = bs.get(id);
String desc = describe(id);
String pushCertField = "pushCertificate";
- diffColumnsExcluding(diffs, PatchSet.class, desc, bundleA, a, bundleB, b, pushCertField);
+
+ boolean excludeDesc = false;
+ if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
+ excludeDesc = Objects.equals(trimOrNull(a.getDescription()), b.getDescription());
+ } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
+ excludeDesc = Objects.equals(a.getDescription(), trimOrNull(b.getDescription()));
+ }
+
+ List<String> exclude = Lists.newArrayList(pushCertField);
+ if (excludeCreatedOn) {
+ exclude.add("createdOn");
+ }
+ if (excludeDesc) {
+ exclude.add("description");
+ }
+
+ diffColumnsExcluding(diffs, PatchSet.class, desc, bundleA, a, bundleB, b, exclude);
diffValues(diffs, desc, trimPushCert(a), trimPushCert(b), pushCertField);
}
}
@@ -659,6 +702,18 @@
return CharMatcher.is('\n').trimTrailingFrom(ps.getPushCertificate());
}
+ private static boolean createdOnIsMonotonic(
+ Map<?, PatchSet> patchSets, Set<PatchSet.Id> limitToIds) {
+ List<PatchSet> orderedById =
+ patchSets
+ .values()
+ .stream()
+ .filter(ps -> limitToIds.contains(ps.getId()))
+ .sorted(ChangeUtil.PS_ID_ORDER)
+ .collect(toList());
+ return Ordering.natural().onResultOf(PatchSet::getCreatedOn).isOrdered(orderedById);
+ }
+
private static void diffPatchSetApprovals(
List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
Map<PatchSetApproval.Key, PatchSetApproval> as = bundleA.filterPatchSetApprovals();
@@ -675,23 +730,43 @@
// actual submit, so the timestamps may not line up. This shouldn't really
// happen, because postSubmit shouldn't be set in ReviewDb until after the
// change is submitted in ReviewDb, but you never know.
+ //
+ // Due to a quirk of PostReview, post-submit 0 votes might not have the
+ // postSubmit bit set in ReviewDb. As these are only used for tombstone
+ // purposes, ignore the postSubmit bit in NoteDb in this case.
Timestamp ta = a.getGranted();
Timestamp tb = b.getGranted();
PatchSet psa = checkNotNull(bundleA.patchSets.get(a.getPatchSetId()));
PatchSet psb = checkNotNull(bundleB.patchSets.get(b.getPatchSetId()));
boolean excludeGranted = false;
+ boolean excludePostSubmit = false;
List<String> exclude = new ArrayList<>(1);
if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
excludeGranted =
(ta.before(psa.getCreatedOn()) && tb.equals(psb.getCreatedOn()))
|| ta.compareTo(tb) < 0;
+ excludePostSubmit = a.getValue() == 0 && b.isPostSubmit();
} else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
excludeGranted =
tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn()) || tb.compareTo(ta) < 0;
+ excludePostSubmit = b.getValue() == 0 && a.isPostSubmit();
}
+
+ // Legacy submit approvals may or may not have tags associated with them,
+ // depending on whether ChangeRebuilder happened to group them with the
+ // status change.
+ boolean excludeTag =
+ bundleA.source != bundleB.source && a.isLegacySubmit() && b.isLegacySubmit();
+
if (excludeGranted) {
exclude.add("granted");
}
+ if (excludePostSubmit) {
+ exclude.add("postSubmit");
+ }
+ if (excludeTag) {
+ exclude.add("tag");
+ }
diffColumnsExcluding(diffs, PatchSetApproval.class, desc, bundleA, a, bundleB, b, exclude);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 425575e..86aebf1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -20,6 +20,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.primitives.Ints;
@@ -619,4 +620,17 @@
name.append('>');
appendHeaderField(writer, header, name.toString());
}
+
+ private static final CharMatcher INVALID_FOOTER_CHARS = CharMatcher.anyOf("\r\n\0");
+
+ static String sanitizeFooter(String value) {
+ // Remove characters that would confuse JGit's footer parser if they were
+ // included in footer values, for example by splitting the footer block into
+ // multiple paragraphs.
+ //
+ // One painful example: RevCommit#getShorMessage() might return a message
+ // containing "\r\r", which RevCommit#getFooterLines() will treat as an
+ // empty paragraph for the purposes of footer parsing.
+ return INVALID_FOOTER_CHARS.trimAndCollapseFrom(value, ' ');
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 068f368..c241965 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -36,6 +36,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.sanitizeFooter;
import static java.util.Comparator.comparing;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -647,6 +648,7 @@
for (Table.Cell<String, Account.Id, Optional<Short>> c : approvals.cellSet()) {
addFooter(msg, FOOTER_LABEL);
+ // Label names/values are safe to append without sanitizing.
if (!c.getValue().isPresent()) {
msg.append('-').append(c.getRowKey());
} else {
@@ -673,6 +675,7 @@
if (rec.labels != null) {
for (SubmitRecord.Label label : rec.labels) {
+ // Label names/values are safe to append without sanitizing.
addFooter(msg, FOOTER_SUBMITTED_WITH)
.append(label.status)
.append(": ")
@@ -764,7 +767,7 @@
private static void addFooter(StringBuilder sb, FooterKey footer, Object... values) {
addFooter(sb, footer);
for (Object value : values) {
- sb.append(value);
+ sb.append(sanitizeFooter(Objects.toString(value)));
}
sb.append('\n');
}
@@ -779,8 +782,4 @@
sb.append('>');
return sb;
}
-
- private static String sanitizeFooter(String value) {
- return value.replace('\n', ' ').replace('\0', ' ');
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
index b369281..9ecf476 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.notedb.rebuild;
+import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.server.notedb.ChangeUpdate;
import java.sql.Timestamp;
@@ -38,6 +39,14 @@
}
@Override
+ protected boolean canHaveTag() {
+ // Legacy SUBM approvals don't have a tag field set, but the corresponding
+ // ChangeMessage for merging the change does. We need to let these be in the
+ // same meta commit so the SUBM approval isn't counted as post-submit.
+ return !psa.isLegacySubmit();
+ }
+
+ @Override
void apply(ChangeUpdate update) {
checkUpdate(update);
update.putApproval(psa.getLabel(), psa.getValue());
@@ -47,4 +56,9 @@
protected boolean isPostSubmitApproval() {
return psa.isPostSubmit();
}
+
+ @Override
+ protected void addToString(ToStringHelper helper) {
+ helper.add("approval", psa);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
index 84858b4e..54303fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
@@ -14,24 +14,39 @@
package com.google.gerrit.server.notedb.rebuild;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gwtorm.server.OrmException;
import java.sql.Timestamp;
+import java.util.Map;
+import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class ChangeMessageEvent extends Event {
+ private static final ImmutableMap<Change.Status, Pattern> STATUS_PATTERNS =
+ ImmutableMap.of(
+ Change.Status.ABANDONED, Pattern.compile("^Abandoned(\n.*)*$"),
+ Change.Status.MERGED,
+ Pattern.compile(
+ "^Change has been successfully" + " (merged|cherry-picked|rebased|pushed).*$"),
+ Change.Status.NEW, Pattern.compile("^Restored(\n.*)*$"));
+
private static final Pattern TOPIC_SET_REGEXP = Pattern.compile("^Topic set to (.+)$");
private static final Pattern TOPIC_CHANGED_REGEXP =
Pattern.compile("^Topic changed from (.+) to (.+)$");
private static final Pattern TOPIC_REMOVED_REGEXP = Pattern.compile("^Topic (.+) removed$");
- private final ChangeMessage message;
+ private final Change change;
private final Change noteDbChange;
+ private final Optional<Change.Status> status;
+ private final ChangeMessage message;
- ChangeMessageEvent(ChangeMessage message, Change noteDbChange, Timestamp changeCreatedOn) {
+ ChangeMessageEvent(
+ Change change, Change noteDbChange, ChangeMessage message, Timestamp changeCreatedOn) {
super(
message.getPatchSetId(),
message.getAuthor(),
@@ -39,8 +54,10 @@
message.getWrittenOn(),
changeCreatedOn,
message.getTag());
- this.message = message;
+ this.change = change;
this.noteDbChange = noteDbChange;
+ this.message = message;
+ this.status = parseStatus(message);
}
@Override
@@ -49,10 +66,44 @@
}
@Override
+ protected boolean isSubmit() {
+ return status.isPresent() && status.get() == Change.Status.MERGED;
+ }
+
+ @Override
+ protected boolean canHaveTag() {
+ return true;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
void apply(ChangeUpdate update) throws OrmException {
checkUpdate(update);
update.setChangeMessage(message.getMessage());
setTopic(update);
+
+ if (status.isPresent()) {
+ Change.Status s = status.get();
+ update.fixStatus(s);
+ noteDbChange.setStatus(s);
+ if (s == Change.Status.MERGED) {
+ update.setSubmissionId(change.getSubmissionId());
+ noteDbChange.setSubmissionId(change.getSubmissionId());
+ }
+ }
+ }
+
+ private static Optional<Change.Status> parseStatus(ChangeMessage message) {
+ String msg = message.getMessage();
+ if (msg == null) {
+ return Optional.empty();
+ }
+ for (Map.Entry<Change.Status, Pattern> e : STATUS_PATTERNS.entrySet()) {
+ if (e.getValue().matcher(msg).matches()) {
+ return Optional.of(e.getKey());
+ }
+ }
+ return Optional.empty();
}
private void setTopic(ChangeUpdate update) {
@@ -81,4 +132,9 @@
noteDbChange.setTopic(null);
}
}
+
+ @Override
+ protected void addToString(ToStringHelper helper) {
+ helper.add("message", message);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index 326ceb3..10f4dd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -27,7 +27,6 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
@@ -77,11 +76,12 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.Iterator;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.TreeMap;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -304,8 +304,8 @@
deleteDraftRefs(change, manager.getAllUsersRepo());
Integer minPsNum = getMinPatchSetNum(bundle);
- Map<PatchSet.Id, PatchSetEvent> patchSetEvents =
- Maps.newHashMapWithExpectedSize(bundle.getPatchSets().size());
+ TreeMap<PatchSet.Id, PatchSetEvent> patchSetEvents =
+ new TreeMap<>(ReviewDbUtil.intKeyOrdering());
for (PatchSet ps : bundle.getPatchSets()) {
PatchSetEvent pse = new PatchSetEvent(change, ps, manager.getChangeRepo().rw);
@@ -320,6 +320,7 @@
draftCommentEvents.put(c.author.getId(), e);
}
}
+ ensurePatchSetOrder(patchSetEvents);
for (PatchSetApproval psa : bundle.getPatchSetApprovals()) {
PatchSetEvent pse = patchSetEvents.get(psa.getPatchSetId());
@@ -335,17 +336,15 @@
Change noteDbChange = new Change(null, null, null, null, null);
for (ChangeMessage msg : bundle.getChangeMessages()) {
- List<Event> msgEvents = parseChangeMessage(msg, change, noteDbChange);
+ Event msgEvent = new ChangeMessageEvent(change, noteDbChange, msg, change.getCreatedOn());
if (msg.getPatchSetId() != null) {
PatchSetEvent pse = patchSetEvents.get(msg.getPatchSetId());
if (pse == null) {
continue; // Ignore events for missing patch sets.
}
- for (Event e : msgEvents) {
- e.addDep(pse);
- }
+ msgEvent.addDep(pse);
}
- events.addAll(msgEvents);
+ events.add(msgEvent);
}
sortAndFillEvents(change, noteDbChange, bundle.getPatchSets(), events, minPsNum);
@@ -373,16 +372,6 @@
}
}
- private List<Event> parseChangeMessage(ChangeMessage msg, Change change, Change noteDbChange) {
- List<Event> events = new ArrayList<>(2);
- events.add(new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn()));
- Optional<StatusChangeEvent> sce = StatusChangeEvent.parseFromMessage(msg, change, noteDbChange);
- if (sce.isPresent()) {
- events.add(sce.get());
- }
- return events;
- }
-
private static Integer getMinPatchSetNum(ChangeBundle bundle) {
Integer minPsNum = null;
for (PatchSet ps : bundle.getPatchSets()) {
@@ -394,6 +383,19 @@
return minPsNum;
}
+ private static void ensurePatchSetOrder(TreeMap<PatchSet.Id, PatchSetEvent> events) {
+ if (events.isEmpty()) {
+ return;
+ }
+ Iterator<PatchSetEvent> it = events.values().iterator();
+ PatchSetEvent curr = it.next();
+ while (it.hasNext()) {
+ PatchSetEvent next = it.next();
+ next.addDep(curr);
+ curr = next;
+ }
+ }
+
private static List<Comment> getComments(
ChangeBundle bundle, String serverId, PatchLineComment.Status status, PatchSet ps) {
return bundle
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
index 1be5598..c8a649e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchLineComment;
@@ -51,6 +52,11 @@
}
@Override
+ protected boolean canHaveTag() {
+ return true;
+ }
+
+ @Override
void apply(ChangeUpdate update) throws OrmException {
checkUpdate(update);
if (c.revId == null) {
@@ -58,4 +64,9 @@
}
update.putComment(PatchLineComment.Status.PUBLISHED, c);
}
+
+ @Override
+ protected void addToString(ToStringHelper helper) {
+ helper.add("message", c.message);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
index 3cb3eb5..914930c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -61,4 +62,9 @@
}
draftUpdate.putComment(c);
}
+
+ @Override
+ protected void addToString(ToStringHelper helper) {
+ helper.add("message", c.message);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
index 10adbab..3957c5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl.MAX_WINDOW_MS;
import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ComparisonChain;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -102,16 +103,26 @@
return false;
}
+ protected boolean canHaveTag() {
+ return false;
+ }
+
@Override
public String toString() {
- return MoreObjects.toStringHelper(this)
- .add("psId", psId)
- .add("effectiveUser", user)
- .add("realUser", realUser)
- .add("when", when)
- .toString();
+ ToStringHelper helper =
+ MoreObjects.toStringHelper(this)
+ .add("psId", psId)
+ .add("effectiveUser", user)
+ .add("realUser", realUser)
+ .add("when", when)
+ .add("tag", tag);
+ addToString(helper);
+ return helper.toString();
}
+ /** @param helper toString helper to add fields to */
+ protected void addToString(ToStringHelper helper) {}
+
@Override
public int compareTo(Event other) {
return ComparisonChain.start()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
index f0faef1..773215e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
@@ -18,6 +18,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSet;
import java.sql.Timestamp;
@@ -61,9 +62,12 @@
Event last = getLast();
if (!Objects.equals(e.user, last.user)
|| !Objects.equals(e.realUser, last.realUser)
- || !e.psId.equals(last.psId)
- || !Objects.equals(e.tag, last.tag)) {
- return false; // Different patch set, author, or tag.
+ || !e.psId.equals(last.psId)) {
+ return false; // Different patch set or author.
+ }
+ if (e.canHaveTag() && canHaveTag() && !Objects.equals(e.tag, getTag())) {
+ // We should trust the tag field, and it doesn't match.
+ return false;
}
if (e.isPostSubmitApproval() && isSubmit) {
// Post-submit approvals must come after the update that submits.
@@ -132,7 +136,16 @@
}
String getTag() {
- return getLast().tag;
+ for (E e : Lists.reverse(list)) {
+ if (e.tag != null) {
+ return e.tag;
+ }
+ }
+ return null;
+ }
+
+ private boolean canHaveTag() {
+ return list.stream().anyMatch(Event::canHaveTag);
}
private E get(int i) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
index 474a7e0..b1bd6ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
+import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableCollection;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -52,7 +53,7 @@
if (!Objects.equals(change.getTopic(), noteDbChange.getTopic())) {
update.setTopic(change.getTopic());
}
- if (!Objects.equals(change.getStatus(), noteDbChange.getStatus())) {
+ if (!statusMatches()) {
// TODO(dborowitz): Stamp approximate approvals at this time.
update.fixStatus(change.getStatus());
}
@@ -71,6 +72,10 @@
}
}
+ private boolean statusMatches() {
+ return Objects.equals(change.getStatus(), noteDbChange.getStatus());
+ }
+
private boolean highestNumberedPatchSetIsCurrent() {
PatchSet.Id max = patchSets.stream().map(PatchSet::getId).max(intKeyOrdering()).get();
return max.equals(change.currentPatchSetId());
@@ -80,4 +85,11 @@
protected boolean isSubmit() {
return change.getStatus() == Change.Status.MERGED;
}
+
+ @Override
+ protected void addToString(ToStringHelper helper) {
+ if (!statusMatches()) {
+ helper.add("status", change.getStatus());
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
index 028abfe..4f6f6ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.notedb.rebuild;
+import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -53,4 +54,9 @@
void apply(ChangeUpdate update) throws OrmException {
update.setHashtags(hashtags);
}
+
+ @Override
+ protected void addToString(ToStringHelper helper) {
+ helper.add("hashtags", hashtags);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
index 1d8fbfb..2ecf969 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.notedb.rebuild;
+import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.Table;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -54,4 +55,9 @@
checkUpdate(update);
update.putReviewer(reviewer.getColumnKey(), reviewer.getRowKey());
}
+
+ @Override
+ protected void addToString(ToStringHelper helper) {
+ helper.add("account", reviewer.getColumnKey()).add("state", reviewer.getRowKey());
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
deleted file mode 100644
index 025b6c8..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (C) 2016 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.notedb.rebuild;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.reviewdb.client.Account;
-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.server.notedb.ChangeUpdate;
-import com.google.gwtorm.server.OrmException;
-import java.sql.Timestamp;
-import java.util.Map;
-import java.util.Optional;
-import java.util.regex.Pattern;
-
-class StatusChangeEvent extends Event {
- private static final ImmutableMap<Change.Status, Pattern> PATTERNS =
- ImmutableMap.of(
- Change.Status.ABANDONED, Pattern.compile("^Abandoned(\n.*)*$"),
- Change.Status.MERGED,
- Pattern.compile(
- "^Change has been successfully" + " (merged|cherry-picked|rebased|pushed).*$"),
- Change.Status.NEW, Pattern.compile("^Restored(\n.*)*$"));
-
- static Optional<StatusChangeEvent> parseFromMessage(
- ChangeMessage message, Change change, Change noteDbChange) {
- String msg = message.getMessage();
- if (msg == null) {
- return Optional.empty();
- }
- for (Map.Entry<Change.Status, Pattern> e : PATTERNS.entrySet()) {
- if (e.getValue().matcher(msg).matches()) {
- return Optional.of(new StatusChangeEvent(message, change, noteDbChange, e.getKey()));
- }
- }
- return Optional.empty();
- }
-
- private final Change.Status status;
- private final Change change;
- private final Change noteDbChange;
-
- private StatusChangeEvent(
- ChangeMessage message, Change change, Change noteDbChange, Change.Status status) {
- this(
- message.getPatchSetId(),
- message.getAuthor(),
- message.getWrittenOn(),
- change,
- noteDbChange,
- message.getTag(),
- status);
- }
-
- private StatusChangeEvent(
- PatchSet.Id psId,
- Account.Id author,
- Timestamp when,
- Change change,
- Change noteDbChange,
- String tag,
- Change.Status status) {
- super(psId, author, author, when, change.getCreatedOn(), tag);
- this.change = change;
- this.noteDbChange = noteDbChange;
- this.status = status;
- }
-
- @Override
- boolean uniquePerUpdate() {
- return true;
- }
-
- @SuppressWarnings("deprecation")
- @Override
- void apply(ChangeUpdate update) throws OrmException {
- checkUpdate(update);
- update.fixStatus(status);
- noteDbChange.setStatus(status);
- if (status == Change.Status.MERGED) {
- update.setSubmissionId(change.getSubmissionId());
- noteDbChange.setSubmissionId(change.getSubmissionId());
- }
- }
-
- @Override
- protected boolean isSubmit() {
- return status == Change.Status.MERGED;
- }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
index 2c60b5d..76995d5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
@@ -62,11 +62,11 @@
b.subject("");
StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.append(toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid" + "\n");
- stringBuilder.append(toFooterWithDelimiter(MetadataName.PATCH_SET) + "1" + "\n");
+ stringBuilder.append(toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid" + "\r\n");
+ stringBuilder.append("> " + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1" + "\n");
stringBuilder.append(toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment" + "\n");
stringBuilder.append(
- toFooterWithDelimiter(MetadataName.TIMESTAMP) + "Tue, 25 Oct 2016 02:11:35 -0700" + "\n");
+ toFooterWithDelimiter(MetadataName.TIMESTAMP) + "Tue, 25 Oct 2016 02:11:35 -0700" + "\r\n");
b.textContent(stringBuilder.toString());
Address author = new Address("Diffy", "test@gerritcodereview.com");
@@ -91,15 +91,16 @@
b.subject("");
StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.append("<p>" + toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid" + "</p>");
- stringBuilder.append("<p>" + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1" + "</p>");
stringBuilder.append(
- "<p>" + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment" + "</p>");
+ "<div id\"someid\">" + toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid" + "</div>");
+ stringBuilder.append("<div>" + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1" + "</div>");
stringBuilder.append(
- "<p>"
+ "<div>" + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment" + "</div>");
+ stringBuilder.append(
+ "<div>"
+ toFooterWithDelimiter(MetadataName.TIMESTAMP)
+ "Tue, 25 Oct 2016 02:11:35 -0700"
- + "</p>");
+ + "</div>");
b.htmlContent(stringBuilder.toString());
Address author = new Address("Diffy", "test@gerritcodereview.com");
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
index a3f8a76..916ba9fc 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
@@ -248,6 +248,55 @@
}
@Test
+ public void diffChangesSanitizesSubjectsBeforeComparison() throws Exception {
+ Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
+ c1.setCurrentPatchSet(c1.currentPatchSetId(), "Subject\r\rbody", "Original");
+ Change c2 = clone(c1);
+ c2.setCurrentPatchSet(c2.currentPatchSetId(), "Subject body", "Original");
+
+ // Both ReviewDb, exact match required
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Subject\r\rbody} != {Subject body}");
+
+ // Both NoteDb, exact match required (although it should be impossible to
+ // create a NoteDb change with '\r' in the subject).
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "subject differs for Change.Id "
+ + c1.getId()
+ + ":"
+ + " {Subject\r\rbody} != {Subject body}");
+
+ // One ReviewDb, one NoteDb, '\r' is normalized to ' '.
+ b1 =
+ new ChangeBundle(
+ c1, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+ }
+
+ @Test
public void diffChangesConsidersEmptyReviewDbTopicEquivalentToNullInNoteDb() throws Exception {
Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
c1.setTopic("");
@@ -306,9 +355,9 @@
}
@Test
- public void diffChangesIgnoresLeadingWhitespaceInReviewDbTopics() throws Exception {
+ public void diffChangesIgnoresLeadingAndTrailingWhitespaceInReviewDbTopics() throws Exception {
Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
- c1.setTopic(" abc");
+ c1.setTopic(" abc ");
Change c2 = clone(c1);
c2.setTopic("abc");
@@ -319,7 +368,7 @@
ChangeBundle b2 =
new ChangeBundle(
c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
- assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ":" + " { abc} != {abc}");
+ assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ":" + " { abc } != {abc}");
// Leading whitespace in ReviewDb topic is ignored.
b1 =
@@ -331,7 +380,7 @@
assertNoDiffs(b1, b2);
assertNoDiffs(b2, b1);
- // Must match except for the leading whitespace.
+ // Must match except for the leading/trailing whitespace.
Change c3 = clone(c1);
c3.setTopic("cba");
b1 =
@@ -340,7 +389,7 @@
b2 =
new ChangeBundle(
c3, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
- assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ":" + " { abc} != {cba}");
+ assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ":" + " { abc } != {cba}");
}
@Test
@@ -1196,6 +1245,157 @@
}
@Test
+ public void diffPatchSetsIgnoresLeadingAndTrailingWhitespaceInReviewDbDescriptions()
+ throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+
+ PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
+ ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ ps1.setUploader(accountId);
+ ps1.setCreatedOn(TimeUtil.nowTs());
+ ps1.setDescription(" abc ");
+ PatchSet ps2 = clone(ps1);
+ ps2.setDescription("abc");
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ":" + " { abc } != {abc}");
+
+ // Whitespace in ReviewDb description is ignored.
+ b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ // Must match except for the leading/trailing whitespace.
+ PatchSet ps3 = clone(ps1);
+ ps3.setDescription("cba");
+ b1 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps1), approvals(), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c, messages(), patchSets(ps3), approvals(), comments(), reviewers(), NOTE_DB);
+ assertDiffs(
+ b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ":" + " { abc } != {cba}");
+ }
+
+ @Test
+ public void diffPatchSetsIgnoresCreatedOnWhenReviewDbIsNonMonotonic() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+
+ Timestamp beforePs1 = TimeUtil.nowTs();
+
+ PatchSet goodPs1 = new PatchSet(new PatchSet.Id(c.getId(), 1));
+ goodPs1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ goodPs1.setUploader(accountId);
+ goodPs1.setCreatedOn(TimeUtil.nowTs());
+ PatchSet goodPs2 = new PatchSet(new PatchSet.Id(c.getId(), 2));
+ goodPs2.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ goodPs2.setUploader(accountId);
+ goodPs2.setCreatedOn(TimeUtil.nowTs());
+ assertThat(goodPs2.getCreatedOn()).isGreaterThan(goodPs1.getCreatedOn());
+
+ PatchSet badPs2 = clone(goodPs2);
+ badPs2.setCreatedOn(beforePs1);
+ assertThat(badPs2.getCreatedOn()).isLessThan(goodPs1.getCreatedOn());
+
+ // Both ReviewDb, exact match required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, goodPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, badPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "createdOn differs for PatchSet.Id "
+ + badPs2.getId()
+ + ":"
+ + " {2009-09-30 17:00:18.0} != {2009-09-30 17:00:06.0}");
+
+ // Non-monotonic in ReviewDb but monotonic in NoteDb, timestamps are
+ // ignored, including for ps1.
+ PatchSet badPs1 = clone(goodPs1);
+ badPs1.setCreatedOn(TimeUtil.nowTs());
+ b1 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(badPs1, badPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, goodPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ // Non-monotonic in NoteDb but monotonic in ReviewDb, timestamps are not
+ // ignored.
+ b1 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(goodPs1, goodPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ REVIEW_DB);
+ b2 =
+ new ChangeBundle(
+ c,
+ messages(),
+ patchSets(badPs1, badPs2),
+ approvals(),
+ comments(),
+ reviewers(),
+ NOTE_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "createdOn differs for PatchSet.Id "
+ + badPs1.getId()
+ + " in NoteDb vs. ReviewDb:"
+ + " {2009-09-30 17:00:24.0} != {2009-09-30 17:00:12.0}",
+ "createdOn differs for PatchSet.Id "
+ + badPs2.getId()
+ + " in NoteDb vs. ReviewDb:"
+ + " {2009-09-30 17:00:06.0} != {2009-09-30 17:00:18.0}");
+ }
+
+ @Test
public void diffPatchSetApprovalKeySets() throws Exception {
Change c = TestChanges.newChange(project, accountId);
int id = c.getId().get();
@@ -1349,6 +1549,62 @@
}
@Test
+ public void diffPatchSetApprovalsIgnoresPostSubmitBitOnZeroVote() throws Exception {
+ Change c = TestChanges.newChange(project, accountId);
+ c.setStatus(Change.Status.MERGED);
+ PatchSetApproval a1 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(c.currentPatchSetId(), accountId, new LabelId("Code-Review")),
+ (short) 0,
+ TimeUtil.nowTs());
+ a1.setPostSubmit(false);
+ PatchSetApproval a2 = clone(a1);
+ a2.setPostSubmit(true);
+
+ // Both are ReviewDb, exact match is required.
+ ChangeBundle b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
+ ChangeBundle b2 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a2), comments(), reviewers(), REVIEW_DB);
+ assertDiffs(
+ b1,
+ b2,
+ "postSubmit differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review:"
+ + " {false} != {true}");
+
+ // One NoteDb, postSubmit is ignored.
+ b1 =
+ new ChangeBundle(
+ c, messages(), latest(c), approvals(a1), comments(), reviewers(), REVIEW_DB);
+ b2 =
+ new ChangeBundle(c, messages(), latest(c), approvals(a2), comments(), reviewers(), NOTE_DB);
+ assertNoDiffs(b1, b2);
+ assertNoDiffs(b2, b1);
+
+ // postSubmit is not ignored if vote isn't 0.
+ a1.setValue((short) 1);
+ a2.setValue((short) 1);
+ assertDiffs(
+ b1,
+ b2,
+ "postSubmit differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review:"
+ + " {false} != {true}");
+ assertDiffs(
+ b2,
+ b1,
+ "postSubmit differs for PatchSetApproval.Key "
+ + c.getId()
+ + "%2C1,100,Code-Review:"
+ + " {true} != {false}");
+ }
+
+ @Test
public void diffReviewers() throws Exception {
Change c = TestChanges.newChange(project, accountId);
Timestamp now = TimeUtil.nowTs();