Handle change subjects containing '\r' in NoteDb
JGit's RevCommit#getShortMessage() might return a subject containing "\r\r". If we put this in the Subject footer of a NoteDb commit, which we do in the commit that creates the patch set, then RevCommit#getFooterLines() will treat "\r\r" as a new paragraph, resulting in ignoring some footers. Fix this by sanitizing '\r' to ' ', along with '\n' and '\0', which were already sanitized in other contexts. Change-Id: I449eded06ecbf713dd073f2d8c77dca721ba3d23
This commit is contained in:
committed by
Edwin Kempin
parent
49df12cb7d
commit
4e1f02db91
@@ -1216,6 +1216,22 @@ public class ChangeRebuilderIT extends AbstractDaemonTest {
|
|||||||
rebuilderWrapper.rebuild(db, id);
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
private void assertChangesReadOnly(RestApiException e) throws Exception {
|
private void assertChangesReadOnly(RestApiException e) throws Exception {
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
assertThat(cause).isInstanceOf(UpdateException.class);
|
assertThat(cause).isInstanceOf(UpdateException.class);
|
||||||
|
|||||||
@@ -434,6 +434,7 @@ public class ChangeBundle {
|
|||||||
excludeCreatedOn =
|
excludeCreatedOn =
|
||||||
!timestampsDiffer(bundleA, bundleA.getFirstPatchSetTime(), bundleB, b.getCreatedOn());
|
!timestampsDiffer(bundleA, bundleA.getFirstPatchSetTime(), bundleB, b.getCreatedOn());
|
||||||
aSubj = cleanReviewDbSubject(aSubj);
|
aSubj = cleanReviewDbSubject(aSubj);
|
||||||
|
bSubj = cleanNoteDbSubject(bSubj);
|
||||||
excludeCurrentPatchSetId = !bundleA.validPatchSetPredicate().apply(a.currentPatchSetId());
|
excludeCurrentPatchSetId = !bundleA.validPatchSetPredicate().apply(a.currentPatchSetId());
|
||||||
excludeSubject = bSubj.startsWith(aSubj) || excludeCurrentPatchSetId;
|
excludeSubject = bSubj.startsWith(aSubj) || excludeCurrentPatchSetId;
|
||||||
excludeOrigSubj = true;
|
excludeOrigSubj = true;
|
||||||
@@ -444,6 +445,7 @@ public class ChangeBundle {
|
|||||||
} else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
|
} else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
|
||||||
excludeCreatedOn =
|
excludeCreatedOn =
|
||||||
!timestampsDiffer(bundleA, a.getCreatedOn(), bundleB, bundleB.getFirstPatchSetTime());
|
!timestampsDiffer(bundleA, a.getCreatedOn(), bundleB, bundleB.getFirstPatchSetTime());
|
||||||
|
aSubj = cleanNoteDbSubject(aSubj);
|
||||||
bSubj = cleanReviewDbSubject(bSubj);
|
bSubj = cleanReviewDbSubject(bSubj);
|
||||||
excludeCurrentPatchSetId = !bundleB.validPatchSetPredicate().apply(b.currentPatchSetId());
|
excludeCurrentPatchSetId = !bundleB.validPatchSetPredicate().apply(b.currentPatchSetId());
|
||||||
excludeSubject = aSubj.startsWith(bSubj) || excludeCurrentPatchSetId;
|
excludeSubject = aSubj.startsWith(bSubj) || excludeCurrentPatchSetId;
|
||||||
@@ -501,7 +503,11 @@ public class ChangeBundle {
|
|||||||
if (rn >= 0) {
|
if (rn >= 0) {
|
||||||
s = s.substring(0, rn);
|
s = s.substring(0, rn);
|
||||||
}
|
}
|
||||||
return s;
|
return ChangeNoteUtil.sanitizeFooter(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String cleanNoteDbSubject(String s) {
|
||||||
|
return ChangeNoteUtil.sanitizeFooter(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
|
|||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.CharMatcher;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ListMultimap;
|
import com.google.common.collect.ListMultimap;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
@@ -619,4 +620,17 @@ public class ChangeNoteUtil {
|
|||||||
name.append('>');
|
name.append('>');
|
||||||
appendHeaderField(writer, header, name.toString());
|
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, ' ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_I
|
|||||||
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
|
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_TAG;
|
||||||
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
|
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 java.util.Comparator.comparing;
|
||||||
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
||||||
|
|
||||||
@@ -599,7 +600,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subject != null) {
|
if (subject != null) {
|
||||||
addFooter(msg, FOOTER_SUBJECT, subject);
|
addFooter(msg, FOOTER_SUBJECT, sanitizeFooter(subject));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (branch != null) {
|
if (branch != null) {
|
||||||
@@ -779,8 +780,4 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
|||||||
sb.append('>');
|
sb.append('>');
|
||||||
return sb;
|
return sb;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String sanitizeFooter(String value) {
|
|
||||||
return value.replace('\n', ' ').replace('\0', ' ');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,6 +247,55 @@ public class ChangeBundleTest extends GerritBaseTests {
|
|||||||
assertNoDiffs(b2, b1);
|
assertNoDiffs(b2, b1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Test
|
||||||
public void diffChangesConsidersEmptyReviewDbTopicEquivalentToNullInNoteDb() throws Exception {
|
public void diffChangesConsidersEmptyReviewDbTopicEquivalentToNullInNoteDb() throws Exception {
|
||||||
Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
|
Change c1 = TestChanges.newChange(new Project.NameKey("project"), new Account.Id(100));
|
||||||
|
|||||||
Reference in New Issue
Block a user