Merge "Merge branch 'stable-3.2' into stable-3.3" into stable-3.3

This commit is contained in:
Luca Milanesio
2021-02-04 22:54:43 +00:00
committed by Gerrit Code Review
17 changed files with 274 additions and 89 deletions

View File

@@ -232,6 +232,17 @@ than the default, say `0.8`, will make scenarios wait somewhat less than how the
Scenario development is often done using locally running Gerrit systems under test, which are Scenario development is often done using locally running Gerrit systems under test, which are
sometimes dockerized. sometimes dockerized.
==== Number of users
The `number_of_users` property can be used to scale scenario steps to run with the specified number
of concurrent users. The value of this property remains `1` by default. For example, this sets the
number of concurrent users to 10:
* `-Dcom.google.gerrit.scenarios.number_of_users=10`
This will make scenarios that support the `number_of_users` property to inject that many users
concurrently for load testing.
== How to run tests == How to run tests
Run all tests: Run all tests:

53
e2e-tests/README Executable file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
#
# Example usage only-
# 1. Optional: replace test@mail.com below with your own, reachable locally.
# 2. Use the '>>' operator below instead to not overwrite your known_hosts; keep '>' otherwise.
# 3. Note that appending as proposed above may potentially repeat the same line multiple times.
# 4. Init your local Gerrit test site then start it; you may refer to [1] below.
# 5. Set GIT_HTTP_PASSWORD below to yours, from [2].
# 6. Change to this directory to execute ./README (this executable file) in its own terminal.
# 7. Install sbt if missing, based on your operating system; re-run to compile.
# 8. Optional: add the below generated (displayed) key to your local admin user [3].
# 9. Otherwise keep the lines below that use your existing user ssh keys for admin testing.
# 10. This script assumes the google-sourced version of the example json file [4].
# 11. If running that scenario locally as below reports authentication failures, [4] may be a fork.
# 12. Uncomment any one of the below sbt commands at will; you may add some locally.
# 13. See [5] for how to start using JAVA_OPTS below; you may leave it empty for these sbt commands.
# 14. You can initialize an IDE sbt (Scala) project from/in this root folder; see [6].
#
# [1] https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init
# [2] http://localhost:8080/settings/#HTTPCredentials
# [3] http://localhost:8080/settings/#SSHKeys
# [4] ./src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
# [5] https://gerrit-review.googlesource.com/Documentation/dev-e2e-tests.html#_environment_properties
# [6] https://gerrit-review.googlesource.com/Documentation/dev-e2e-tests.html#_ide_intellij
# DO NOT change this (assumed) directory; force-removed *recursively* below!
gatlingGitKeys=/tmp/ssh-keys
userSshDir=$HOME/.ssh
# Comment this group of lines out if willing to generate other keys as below.
rm -f $gatlingGitKeys
ln -s "$userSshDir" $gatlingGitKeys
# Comment this group of lines out if keys already generated, as either below or above.
#rm -fr $gatlingGitKeys
#mkdir $gatlingGitKeys
#ssh-keygen -m PEM -t rsa -C "test@mail.com" -f $gatlingGitKeys/id_rsa
ssh-keyscan -t rsa -p 29418 localhost > "$userSshDir"/known_hosts
cat $gatlingGitKeys/id_rsa.pub
export GIT_HTTP_USERNAME="admin"
export GIT_HTTP_PASSWORD="TODO"
export JAVA_OPTS="\
"
#-Dx=y \
#sbt clean
#sbt update
sbt compile
#sbt "gatling:testOnly com.google.gerrit.scenarios.CloneUsingBothProtocols"
#sbt "gatling:lastReport"

View File

@@ -0,0 +1,3 @@
{
"revision": "master"
}

View File

@@ -0,0 +1,6 @@
[
{
"url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/PROJECT/branches/",
"project": "PROJECT"
}
]

View File

@@ -21,9 +21,9 @@ import io.gatling.core.structure.ScenarioBuilder
import scala.concurrent.duration._ import scala.concurrent.duration._
class CloneUsingBothProtocols extends GitSimulation { class CloneUsingBothProtocols extends GitSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).queue private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
private val projectName = className private val projectName = className
private val duration = 2 private val duration = 2 * numberOfUsers
override def replaceOverride(in: String): String = { override def replaceOverride(in: String): String = {
replaceKeyWith("_project", projectName, in) replaceKeyWith("_project", projectName, in)
@@ -43,7 +43,7 @@ class CloneUsingBothProtocols extends GitSimulation {
), ),
test.inject( test.inject(
nothingFor(stepWaitTime(this) seconds), nothingFor(stepWaitTime(this) seconds),
constantUsersPerSec(single) during (duration seconds) constantUsersPerSec(numberOfUsers) during (duration seconds)
).protocols(gitProtocol), ).protocols(gitProtocol),
deleteProject.test.inject( deleteProject.test.inject(
nothingFor(stepWaitTime(deleteProject) + duration seconds), nothingFor(stepWaitTime(deleteProject) + duration seconds),

View File

@@ -0,0 +1,56 @@
// Copyright (C) 2021 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.scenarios
import io.gatling.core.Predef.{atOnceUsers, _}
import io.gatling.core.feeder.FeederBuilder
import io.gatling.core.structure.ScenarioBuilder
import io.gatling.http.Predef._
import scala.concurrent.duration._
class CreateBranch extends ProjectSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
private val branchIdKey = "branchId"
private var counter = 0
private val test: ScenarioBuilder = scenario(uniqueName)
.feed(data)
.exec(session => {
counter += 1
session.set(branchIdKey, "branch-" + counter)
})
.exec(http(uniqueName)
.post("${url}${" + branchIdKey + "}")
.body(ElFileBody(body)).asJson)
private val createProject = new CreateProject(projectName)
private val deleteProject = new DeleteProject(projectName)
setUp(
createProject.test.inject(
nothingFor(stepWaitTime(createProject) seconds),
atOnceUsers(single)
),
test.inject(
nothingFor(stepWaitTime(this) seconds),
atOnceUsers(numberOfUsers)
),
deleteProject.test.inject(
nothingFor(stepWaitTime(deleteProject) seconds),
atOnceUsers(single)
),
).protocols(httpProtocol)
}

View File

@@ -19,12 +19,14 @@ import io.gatling.core.feeder.FeederBuilder
import io.gatling.core.structure.ScenarioBuilder import io.gatling.core.structure.ScenarioBuilder
import io.gatling.http.Predef._ import io.gatling.http.Predef._
import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
class CreateChange extends ProjectSimulation { class CreateChange extends ProjectSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).queue private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
private val numberKey = "_number" private val numberKey = "_number"
var number = 0 var number = 0
var numbers: mutable.Queue[Int] = mutable.Queue[Int]()
override def relativeRuntimeWeight = 2 override def relativeRuntimeWeight = 2
@@ -40,6 +42,7 @@ class CreateChange extends ProjectSimulation {
.check(regex("\"" + numberKey + "\":(\\d+),").saveAs(numberKey))) .check(regex("\"" + numberKey + "\":(\\d+),").saveAs(numberKey)))
.exec(session => { .exec(session => {
number = session(numberKey).as[Int] number = session(numberKey).as[Int]
numbers += number
session session
}) })
@@ -54,11 +57,11 @@ class CreateChange extends ProjectSimulation {
), ),
test.inject( test.inject(
nothingFor(stepWaitTime(this) seconds), nothingFor(stepWaitTime(this) seconds),
atOnceUsers(single) atOnceUsers(numberOfUsers)
), ),
deleteChange.test.inject( deleteChange.test.inject(
nothingFor(stepWaitTime(deleteChange) seconds), nothingFor(stepWaitTime(deleteChange) seconds),
atOnceUsers(single) atOnceUsers(numberOfUsers)
), ),
deleteProject.test.inject( deleteProject.test.inject(
nothingFor(stepWaitTime(deleteProject) seconds), nothingFor(stepWaitTime(deleteProject) seconds),

View File

@@ -20,7 +20,7 @@ import io.gatling.core.structure.ScenarioBuilder
import io.gatling.http.Predef.http import io.gatling.http.Predef.http
class DeleteChange extends GerritSimulation { class DeleteChange extends GerritSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).queue private val data: FeederBuilder = jsonFile(resource).convert(keys).circular
private var createChange: Option[CreateChange] = None private var createChange: Option[CreateChange] = None
override def relativeRuntimeWeight = 2 override def relativeRuntimeWeight = 2
@@ -34,7 +34,7 @@ class DeleteChange extends GerritSimulation {
.feed(data) .feed(data)
.exec(session => { .exec(session => {
if (createChange.nonEmpty) { if (createChange.nonEmpty) {
session.set("number", createChange.get.number) session.set("number", createChange.get.numbers.dequeue())
} else { } else {
session session
} }

View File

@@ -34,6 +34,7 @@ class GerritSimulation extends Simulation {
protected val uniqueName: String = className + "-" + hashCode() protected val uniqueName: String = className + "-" + hashCode()
protected val single = 1 protected val single = 1
val numberOfUsers: Int = replaceProperty("number_of_users", single).toInt
val replicationDelay: Int = replaceProperty("replication_delay", 15).toInt val replicationDelay: Int = replaceProperty("replication_delay", 15).toInt
private val powerFactor = replaceProperty("power_factor", 1.0).toDouble private val powerFactor = replaceProperty("power_factor", 1.0).toDouble
protected val SecondsPerWeightUnit = 2 protected val SecondsPerWeightUnit = 2

View File

@@ -47,7 +47,6 @@ import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.base.Strings; import com.google.common.base.Strings;
@@ -140,7 +139,6 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
@@ -1863,10 +1861,6 @@ public class RestApiServlet extends HttpServlet {
static long replyText( static long replyText(
@Nullable HttpServletRequest req, HttpServletResponse res, boolean allowTracing, String text) @Nullable HttpServletRequest req, HttpServletResponse res, boolean allowTracing, String text)
throws IOException { throws IOException {
if ((req == null || isRead(req)) && isMaybeHTML(text)) {
return replyJson(
req, res, allowTracing, ImmutableListMultimap.of("pp", "0"), new JsonPrimitive(text));
}
if (!text.endsWith("\n")) { if (!text.endsWith("\n")) {
text += "\n"; text += "\n";
} }
@@ -1876,10 +1870,6 @@ public class RestApiServlet extends HttpServlet {
return replyBinaryResult(req, res, BinaryResult.create(text).setContentType(PLAIN_TEXT)); return replyBinaryResult(req, res, BinaryResult.create(text).setContentType(PLAIN_TEXT));
} }
private static boolean isMaybeHTML(String text) {
return CharMatcher.anyOf("<&").matchesAnyOf(text);
}
private static boolean acceptsGzip(HttpServletRequest req) { private static boolean acceptsGzip(HttpServletRequest req) {
if (req != null) { if (req != null) {
String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING); String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);

View File

@@ -22,7 +22,10 @@ import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.server.config.UrlFormatter; import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.util.CommitMessageUtil;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
@@ -34,6 +37,8 @@ import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
@@ -109,6 +114,37 @@ public class ChangeUtil {
id); id);
} }
/**
* Make sure that the change commit message has a correct footer.
*
* @param requireChangeId true if Change-Id is a mandatory footer for the project
* @param currentChangeId current Change-Id value before the commit message is updated
* @param newCommitMessage new commit message for the change
* @throws ResourceConflictException if the new commit message has a missing or invalid Change-Id
* @throws BadRequestException if the new commit message is null or empty
*/
public static void ensureChangeIdIsCorrect(
boolean requireChangeId, String currentChangeId, String newCommitMessage)
throws ResourceConflictException, BadRequestException {
RevCommit revCommit =
RevCommit.parse(
Constants.encode("tree " + ObjectId.zeroId().name() + "\n\n" + newCommitMessage));
// Check that the commit message without footers is not empty
CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage());
List<String> changeIdFooters = revCommit.getFooterLines(FooterConstants.CHANGE_ID);
if (requireChangeId && changeIdFooters.isEmpty()) {
throw new ResourceConflictException("missing Change-Id footer");
}
if (!changeIdFooters.isEmpty() && !changeIdFooters.get(0).equals(currentChangeId)) {
throw new ResourceConflictException("wrong Change-Id footer");
}
if (changeIdFooters.size() > 1) {
throw new ResourceConflictException("multiple Change-Id footers");
}
}
public static String status(Change c) { public static String status(Change c) {
return c != null ? c.getStatus().name().toLowerCase() : "deleted"; return c != null ? c.getStatus().name().toLowerCase() : "deleted";
} }

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.server.edit;
import static com.google.gerrit.server.project.ProjectCache.illegalState; import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.Change; import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project; import com.google.gerrit.entities.Project;
@@ -367,8 +368,15 @@ public class ChangeEditModifier {
PatchSet basePatchset = modificationTarget.getBasePatchset(); PatchSet basePatchset = modificationTarget.getBasePatchset();
RevCommit basePatchsetCommit = NoteDbEdits.lookupCommit(repository, basePatchset.commitId()); RevCommit basePatchsetCommit = NoteDbEdits.lookupCommit(repository, basePatchset.commitId());
boolean changeIdRequired =
projectCache
.get(notes.getChange().getProject())
.orElseThrow(illegalState(notes.getChange().getProject()))
.is(BooleanProjectConfig.REQUIRE_CHANGE_ID);
String currentChangeId = notes.getChange().getKey().get();
String newCommitMessage = String newCommitMessage =
createNewCommitMessage(editBehavior, commitModification, commitToModify); createNewCommitMessage(
changeIdRequired, currentChangeId, editBehavior, commitModification, commitToModify);
newCommitMessage = editBehavior.mergeCommitMessageIfNecessary(newCommitMessage, commitToModify); newCommitMessage = editBehavior.mergeCommitMessageIfNecessary(newCommitMessage, commitToModify);
Optional<ChangeEdit> unmodifiedEdit = Optional<ChangeEdit> unmodifiedEdit =
@@ -464,8 +472,12 @@ public class ChangeEditModifier {
} }
private String createNewCommitMessage( private String createNewCommitMessage(
EditBehavior editBehavior, CommitModification commitModification, RevCommit commitToModify) boolean requireChangeId,
throws InvalidChangeOperationException, BadRequestException { String currentChangeId,
EditBehavior editBehavior,
CommitModification commitModification,
RevCommit commitToModify)
throws InvalidChangeOperationException, BadRequestException, ResourceConflictException {
if (!commitModification.newCommitMessage().isPresent()) { if (!commitModification.newCommitMessage().isPresent()) {
return editBehavior.getUnmodifiedCommitMessage(commitToModify); return editBehavior.getUnmodifiedCommitMessage(commitToModify);
} }
@@ -479,6 +491,8 @@ public class ChangeEditModifier {
"New commit message cannot be same as existing commit message"); "New commit message cannot be same as existing commit message");
} }
ChangeUtil.ensureChangeIdIsCorrect(requireChangeId, currentChangeId, newCommitMessage);
return newCommitMessage; return newCommitMessage;
} }

View File

@@ -16,7 +16,6 @@ package com.google.gerrit.server.restapi.change;
import static com.google.gerrit.server.project.ProjectCache.illegalState; import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.entities.BooleanProjectConfig; import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -51,11 +50,9 @@ import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.List;
import java.util.TimeZone; import java.util.TimeZone;
import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
@@ -116,14 +113,13 @@ public class PutMessage implements RestModifyView<ChangeResource, CommitMessageI
String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message); String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message);
ensureCanEditCommitMessage(resource.getNotes()); ensureCanEditCommitMessage(resource.getNotes());
sanitizedCommitMessage = ChangeUtil.ensureChangeIdIsCorrect(
ensureChangeIdIsCorrect( projectCache
projectCache .get(resource.getProject())
.get(resource.getProject()) .orElseThrow(illegalState(resource.getProject()))
.orElseThrow(illegalState(resource.getProject())) .is(BooleanProjectConfig.REQUIRE_CHANGE_ID),
.is(BooleanProjectConfig.REQUIRE_CHANGE_ID), resource.getChange().getKey().get(),
resource.getChange().getKey().get(), sanitizedCommitMessage);
sanitizedCommitMessage);
try (Repository repository = repositoryManager.openRepository(resource.getProject()); try (Repository repository = repositoryManager.openRepository(resource.getProject());
RevWalk revWalk = new RevWalk(repository); RevWalk revWalk = new RevWalk(repository);
@@ -204,33 +200,4 @@ public class PutMessage implements RestModifyView<ChangeResource, CommitMessageI
throw new AuthException("modifying commit message not permitted", denied); throw new AuthException("modifying commit message not permitted", denied);
} }
} }
private String ensureChangeIdIsCorrect(
boolean requireChangeId, String currentChangeId, String newCommitMessage)
throws ResourceConflictException, BadRequestException {
RevCommit revCommit =
RevCommit.parse(
Constants.encode("tree " + ObjectId.zeroId().name() + "\n\n" + newCommitMessage));
// Check that the commit message without footers is not empty
CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage());
List<String> changeIdFooters = ChangeUtil.getChangeIdsFromFooter(revCommit, urlFormatter.get());
if (!changeIdFooters.isEmpty() && !changeIdFooters.get(0).equals(currentChangeId)) {
throw new ResourceConflictException("wrong Change-Id footer");
}
if (requireChangeId && revCommit.getFooterLines().isEmpty()) {
// sanitization always adds '\n' at the end.
newCommitMessage += "\n";
}
if (requireChangeId && changeIdFooters.isEmpty()) {
newCommitMessage += FooterConstants.CHANGE_ID.getName() + ": " + currentChangeId + "\n";
} else if (changeIdFooters.size() > 1) {
throw new ResourceConflictException("multiple Change-Id footers");
}
return newCommitMessage;
}
} }

View File

@@ -3666,16 +3666,6 @@ public class ChangeIT extends AbstractDaemonTest {
assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage); assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
} }
@Test
public void changeCommitMessageWithNoChangeIdRetainsChangeID() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(getCommitMessage(r.getChangeId()))
.isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
gApi.changes().id(r.getChangeId()).setMessage("modified commit\n");
assertThat(getCommitMessage(r.getChangeId()))
.isEqualTo("modified commit\n\nChange-Id: " + r.getChangeId() + "\n");
}
@Test @Test
public void changeCommitMessageNullNotAllowed() throws Exception { public void changeCommitMessageNullNotAllowed() throws Exception {
PushOneCommit.Result r = createChange(); PushOneCommit.Result r = createChange();

View File

@@ -470,7 +470,7 @@ public class RevisionDiffIT extends AbstractDaemonTest {
gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent)); gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset"); gApi.changes().id(changeId).edit().modifyCommitMessage(updatedCommitMessage());
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
DiffInfo diffInfo = DiffInfo diffInfo =
@@ -492,7 +492,7 @@ public class RevisionDiffIT extends AbstractDaemonTest {
gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent)); gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset"); gApi.changes().id(changeId).edit().modifyCommitMessage(updatedCommitMessage());
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
DiffInfo diffInfo = DiffInfo diffInfo =
@@ -510,7 +510,7 @@ public class RevisionDiffIT extends AbstractDaemonTest {
gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent)); gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset"); gApi.changes().id(changeId).edit().modifyCommitMessage(updatedCommitMessage());
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
DiffInfo diffInfo = DiffInfo diffInfo =
@@ -2704,6 +2704,10 @@ public class RevisionDiffIT extends AbstractDaemonTest {
assertThat(e).hasMessageThat().isEqualTo("edit not allowed as base"); assertThat(e).hasMessageThat().isEqualTo("edit not allowed as base");
} }
private String updatedCommitMessage() {
return "An unchanged patchset\n\nChange-Id: " + changeId;
}
private void assertDiffForNewFile( private void assertDiffForNewFile(
PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception { PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
DiffInfo diff = DiffInfo diff =

View File

@@ -1046,7 +1046,8 @@ public class RobotCommentsIT extends AbstractDaemonTest {
@Test @Test
public void fixDoesNotModifyCommitMessageOfChangeEdit() throws Exception { public void fixDoesNotModifyCommitMessageOfChangeEdit() throws Exception {
String changeEditCommitMessage = "This is the commit message of the change edit.\n"; String changeEditCommitMessage =
"This is the commit message of the change edit.\n\nChange-Id: " + changeId + "\n";
gApi.changes().id(changeId).edit().modifyCommitMessage(changeEditCommitMessage); gApi.changes().id(changeId).edit().modifyCommitMessage(changeEditCommitMessage);
fixReplacementInfo.path = FILE_NAME; fixReplacementInfo.path = FILE_NAME;
@@ -1065,7 +1066,8 @@ public class RobotCommentsIT extends AbstractDaemonTest {
@Test @Test
public void fixOnCommitMessageCanBeApplied() throws Exception { public void fixOnCommitMessageCanBeApplied() throws Exception {
// Set a dedicated commit message. // Set a dedicated commit message.
String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n"; String footer = "\nChange-Id: " + changeId + "\n";
String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n" + footer;
gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage); gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
@@ -1080,13 +1082,15 @@ public class RobotCommentsIT extends AbstractDaemonTest {
gApi.changes().id(changeId).current().applyFix(fixId); gApi.changes().id(changeId).current().applyFix(fixId);
String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage(); String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
assertThat(commitMessage).isEqualTo("Modified line\nLine 2 of commit message\n"); assertThat(commitMessage).isEqualTo("Modified line\nLine 2 of commit message\n" + footer);
} }
@Test @Test
public void fixOnHeaderPartOfCommitMessageCannotBeApplied() throws Exception { public void fixOnHeaderPartOfCommitMessageCannotBeApplied() throws Exception {
// Set a dedicated commit message. // Set a dedicated commit message.
String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n"; String footer = "Change-Id: " + changeId;
String originalCommitMessage =
"Line 1 of commit message\nLine 2 of commit message\n" + "\n" + footer + "\n";
gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage); gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
@@ -1108,8 +1112,9 @@ public class RobotCommentsIT extends AbstractDaemonTest {
@Test @Test
public void fixContainingSeveralModificationsOfCommitMessageCanBeApplied() throws Exception { public void fixContainingSeveralModificationsOfCommitMessageCanBeApplied() throws Exception {
// Set a dedicated commit message. // Set a dedicated commit message.
String footer = "\nChange-Id: " + changeId + "\n";
String originalCommitMessage = String originalCommitMessage =
"Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n"; "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n" + footer;
gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage); gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
@@ -1135,13 +1140,14 @@ public class RobotCommentsIT extends AbstractDaemonTest {
String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage(); String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
assertThat(commitMessage) assertThat(commitMessage)
.isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n"); .isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n" + footer);
} }
@Test @Test
public void fixModifyingTheCommitMessageAndAFileCanBeApplied() throws Exception { public void fixModifyingTheCommitMessageAndAFileCanBeApplied() throws Exception {
// Set a dedicated commit message. // Set a dedicated commit message.
String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n"; String footer = "\nChange-Id: " + changeId + "\n";
String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n" + footer;
gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage); gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
@@ -1165,7 +1171,7 @@ public class RobotCommentsIT extends AbstractDaemonTest {
gApi.changes().id(changeId).current().applyFix(fixId); gApi.changes().id(changeId).current().applyFix(fixId);
String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage(); String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
assertThat(commitMessage).isEqualTo("Modified line 1\nLine 2 of commit message\n"); assertThat(commitMessage).isEqualTo("Modified line 1\nLine 2 of commit message\n" + footer);
Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME2); Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
BinaryResultSubject.assertThat(file) BinaryResultSubject.assertThat(file)
.value() .value()
@@ -1176,8 +1182,9 @@ public class RobotCommentsIT extends AbstractDaemonTest {
@Test @Test
public void twoFixesOnCommitMessageCanBeAppliedOneAfterTheOther() throws Exception { public void twoFixesOnCommitMessageCanBeAppliedOneAfterTheOther() throws Exception {
// Set a dedicated commit message. // Set a dedicated commit message.
String footer = "\nChange-Id: " + changeId + "\n";
String originalCommitMessage = String originalCommitMessage =
"Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n"; "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n" + footer;
gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage); gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
@@ -1206,14 +1213,17 @@ public class RobotCommentsIT extends AbstractDaemonTest {
String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage(); String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
assertThat(commitMessage) assertThat(commitMessage)
.isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n"); .isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n" + footer);
} }
@Test @Test
public void twoConflictingFixesOnCommitMessageCanNotBeAppliedOneAfterTheOther() throws Exception { public void twoConflictingFixesOnCommitMessageCanNotBeAppliedOneAfterTheOther() throws Exception {
// Set a dedicated commit message. // Set a dedicated commit message.
String footer = "Change-Id: " + changeId;
String originalCommitMessage = String originalCommitMessage =
"Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n"; "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n\n"
+ footer
+ "\n";
gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage); gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
gApi.changes().id(changeId).edit().publish(); gApi.changes().id(changeId).edit().publish();
@@ -1379,8 +1389,10 @@ public class RobotCommentsIT extends AbstractDaemonTest {
@Test @Test
public void getFixPreviewForCommitMsg() throws Exception { public void getFixPreviewForCommitMsg() throws Exception {
String footer = "Change-Id: " + changeId;
updateCommitMessage( updateCommitMessage(
changeId, "Commit title\n\nCommit message line 1\nLine 2\nLine 3\nLast line\n"); changeId,
"Commit title\n\nCommit message line 1\nLine 2\nLine 3\nLast line\n\n" + footer + "\n");
FixReplacementInfo commitMsgReplacement = new FixReplacementInfo(); FixReplacementInfo commitMsgReplacement = new FixReplacementInfo();
commitMsgReplacement.path = Patch.COMMIT_MSG; commitMsgReplacement.path = Patch.COMMIT_MSG;
// The test assumes that the first 5 lines is a header. // The test assumes that the first 5 lines is a header.
@@ -1420,7 +1432,11 @@ public class RobotCommentsIT extends AbstractDaemonTest {
.isEqualTo("Commit message line 1"); .isEqualTo("Commit message line 1");
assertThat(diff).content().element(1).linesOfA().containsExactly("Line 2"); assertThat(diff).content().element(1).linesOfA().containsExactly("Line 2");
assertThat(diff).content().element(1).linesOfB().containsExactly("New content"); assertThat(diff).content().element(1).linesOfB().containsExactly("New content");
assertThat(diff).content().element(2).commonLines().containsExactly("Line 3", "Last line", ""); assertThat(diff)
.content()
.element(2)
.commonLines()
.containsExactly("Line 3", "Last line", "", footer, "");
} }
private void updateCommitMessage(String changeId, String newCommitMessage) throws Exception { private void updateCommitMessage(String changeId, String newCommitMessage) throws Exception {

View File

@@ -310,12 +310,14 @@ public class ChangeEditIT extends AbstractDaemonTest {
@Test @Test
public void updateCommitMessageByEditingMagicCommitMsgFile() throws Exception { public void updateCommitMessageByEditingMagicCommitMsgFile() throws Exception {
createEmptyEditFor(changeId); createEmptyEditFor(changeId);
String updatedCommitMsg = "Foo Bar\n\nChange-Id: " + changeId + "\n";
gApi.changes() gApi.changes()
.id(changeId) .id(changeId)
.edit() .edit()
.modifyFile(Patch.COMMIT_MSG, RawInputUtil.create("Foo Bar".getBytes(UTF_8))); .modifyFile(Patch.COMMIT_MSG, RawInputUtil.create(updatedCommitMsg.getBytes(UTF_8)));
assertThat(getEdit(changeId)).isPresent(); assertThat(getEdit(changeId)).isPresent();
ensureSameBytes(getFileContentOfEdit(changeId, Patch.COMMIT_MSG), "Foo Bar\n".getBytes(UTF_8)); ensureSameBytes(
getFileContentOfEdit(changeId, Patch.COMMIT_MSG), updatedCommitMsg.getBytes(UTF_8));
} }
@Test @Test
@@ -345,6 +347,39 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(commitMessage).isEqualTo(msg); assertThat(commitMessage).isEqualTo(msg);
} }
@Test
public void updateMessageEditChangeIdShouldThrowResourceConflictException() throws Exception {
createEmptyEditFor(changeId);
String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() ->
gApi.changes()
.id(changeId)
.edit()
.modifyCommitMessage(commitMessage.replaceAll(changeId, changeId2)));
assertThat(thrown).hasMessageThat().isEqualTo("wrong Change-Id footer");
}
@Test
public void updateMessageEditRemoveChangeIdShouldThrowResourceConflictException()
throws Exception {
createEmptyEditFor(changeId);
String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() ->
gApi.changes()
.id(changeId)
.edit()
.modifyCommitMessage(commitMessage.replaceAll("(Change-Id:).*", "")));
assertThat(thrown).hasMessageThat().isEqualTo("missing Change-Id footer");
}
@Test @Test
public void updateMessageNoChange() throws Exception { public void updateMessageNoChange() throws Exception {
createEmptyEditFor(changeId); createEmptyEditFor(changeId);