Merge "Update related changes on topic edit"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index a274206..16e61f6 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3130,6 +3130,28 @@
HTTP/1.1 204 No Content
----
+[[set-message]]
+=== Set Commit Message
+--
+'PUT /changes/link:#change-id[\{change-id\}]/message'
+--
+
+Creates a new patch set with a new commit message.
+
+The new commit message must be provided in the request body inside a
+link:#commit-message-input[CommitMessageInput] entity and contain the change ID footer if
+link:project-configuration.html#require-change-id[Require Change-Id] was specified.
+
+.Request
+----
+ PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "message": "New Commit message \n\nChange-Id: I10394472cbd17dd12454f229e4f6de00b143a444\n"
+ }
+----
+
[[revision-endpoints]]
== Revision Endpoints
@@ -5921,6 +5943,25 @@
link:#web-link-info[WebLinkInfo] entities.
|===========================
+[[commit-message-input]]
+=== CommitMessageInput
+The `CommitMessageInput` entity contains information for changing
+the commit message of a change.
+
+[options="header",cols="1,^1,5"]
+|=============================
+|Field Name ||Description
+|`message` ||New commit message.
+|`notify` |optional|
+Notify handling that defines to whom email notifications should be sent
+after the commit message was updated. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
+|`notify_details`|optional|
+Additional information about whom to notify about the update as a map
+of recipient type to link:#notify-info[NotifyInfo] entity.
+|=============================
+
[[delete-comment-input]]
=== DeleteCommentInput
The `DeleteCommentInput` entity contains the option for deleting a comment.
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index cf09f0c..d2b1f32 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -237,7 +237,7 @@
protected TestRepository<InMemoryRepository> testRepo;
protected String resourcePrefix;
protected Description description;
- protected boolean useSsh;
+ protected boolean testRequiresSsh;
@Inject private ChangeIndexCollection changeIndexes;
@Inject private EventRecorder.Factory eventRecorderFactory;
@@ -259,7 +259,7 @@
@Before
public void assumeSshIfRequired() {
- if (useSsh) {
+ if (testRequiresSsh) {
// If the test uses ssh, we use assume() to make sure ssh is enabled on
// the test suite. JUnit will skip tests annotated with @UseSsh if we
// disable them using the command line flag.
@@ -341,8 +341,10 @@
db = reviewDbProvider.open();
- useSsh = SshMode.useSsh() && (classDesc.useSsh() || methodDesc.useSsh());
- if (useSsh && (adminSshSession == null || userSshSession == null)) {
+ testRequiresSsh = classDesc.useSshAnnotation() || methodDesc.useSshAnnotation();
+ if (testRequiresSsh
+ && SshMode.useSsh()
+ && (adminSshSession == null || userSshSession == null)) {
// Create Ssh sessions
initSsh(admin);
Context ctx = newRequestContext(user);
@@ -1293,4 +1295,13 @@
return revCommit;
}
}
+
+ protected RevCommit parseCurrentRevision(RevWalk rw, PushOneCommit.Result r) throws Exception {
+ return parseCurrentRevision(rw, r.getChangeId());
+ }
+
+ protected RevCommit parseCurrentRevision(RevWalk rw, String changeId) throws Exception {
+ return rw.parseCommit(
+ ObjectId.fromString(get(changeId, ListChangesOption.CURRENT_REVISION).currentRevision));
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 48b66f6..1c5f178 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -211,28 +211,13 @@
@Test
public void create() throws Exception {
- TestAccount foo = accountCreator.create("foo");
- AccountInfo info = gApi.accounts().id(foo.id.get()).get();
- assertThat(info.username).isEqualTo("foo");
- if (useSsh) {
- accountIndexedCounter.assertReindexOf(
- foo, 3); // account creation + external ID creation + adding SSH keys
- } else {
- accountIndexedCounter.assertReindexOf(foo, 2); // account creation + external ID creation
- }
+ create(2); // account creation + external ID creation
+ }
- // check user branch
- try (Repository repo = repoManager.openRepository(allUsers);
- RevWalk rw = new RevWalk(repo)) {
- Ref ref = repo.exactRef(RefNames.refsUsers(foo.getId()));
- assertThat(ref).isNotNull();
- RevCommit c = rw.parseCommit(ref.getObjectId());
- long timestampDiffMs =
- Math.abs(
- c.getCommitTime() * 1000L
- - accountCache.get(foo.getId()).getAccount().getRegisteredOn().getTime());
- assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
- }
+ @Test
+ @UseSsh
+ public void createWithSshKeys() throws Exception {
+ create(3); // account creation + external ID creation + adding SSH keys
}
@Test
@@ -1086,6 +1071,26 @@
assertThat(checkInfo.checkAccountsResult.problems).containsExactlyElementsIn(expectedProblems);
}
+ public void create(int expectedAccountReindexCalls) throws Exception {
+ TestAccount foo = accountCreator.create("foo");
+ AccountInfo info = gApi.accounts().id(foo.id.get()).get();
+ assertThat(info.username).isEqualTo("foo");
+ accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls);
+
+ // check user branch
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ Ref ref = repo.exactRef(RefNames.refsUsers(foo.getId()));
+ assertThat(ref).isNotNull();
+ RevCommit c = rw.parseCommit(ref.getObjectId());
+ long timestampDiffMs =
+ Math.abs(
+ c.getCommitTime() * 1000L
+ - accountCache.get(foo.getId()).getAccount().getRegisteredOn().getTime());
+ assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
+ }
+ }
+
private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
int seq = 1;
for (SshKeyInfo key : sshKeys) {
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 e92d255..429ee0f 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
@@ -52,7 +52,9 @@
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
@@ -75,6 +77,7 @@
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.PatchSetWebLink;
import com.google.gerrit.reviewdb.client.Account;
@@ -82,11 +85,13 @@
import com.google.gerrit.reviewdb.client.Branch.NameKey;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.change.GetRevisionActions;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@@ -99,6 +104,8 @@
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
@@ -1203,6 +1210,84 @@
.containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
}
+ @Test
+ public void changeCommitMessage() throws Exception {
+ // Tests mutating the commit message as both the owner of the change and a regular user with
+ // addPatchSet permission. Asserts that both cases succeed.
+ PushOneCommit.Result r = createChange();
+ r.assertOkStatus();
+ assertThat(getCommitMessage(r.getChangeId()))
+ .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+
+ for (TestAccount acc : ImmutableList.of(admin, user)) {
+ setApiUser(acc);
+ String newMessage =
+ "modified commit by " + acc.username + "\n\nChange-Id: " + r.getChangeId() + "\n";
+ gApi.changes().id(r.getChangeId()).setMessage(newMessage);
+ RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
+ assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
+ assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
+ }
+ }
+
+ @Test
+ public void changeCommitMessageWithNoChangeIdSucceedsIfChangeIdNotRequired() throws Exception {
+ ConfigInput configInput = new ConfigInput();
+ configInput.requireChangeId = InheritableBoolean.FALSE;
+ gApi.projects().name(project.get()).config(configInput);
+
+ PushOneCommit.Result r = createChange();
+ r.assertOkStatus();
+ assertThat(getCommitMessage(r.getChangeId()))
+ .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+
+ String newMessage = "modified commit\n";
+ gApi.changes().id(r.getChangeId()).setMessage(newMessage);
+ RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
+ assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
+ assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
+ }
+
+ @Test
+ public void changeCommitMessageWithNoChangeIdFails() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(getCommitMessage(r.getChangeId()))
+ .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("missing Change-Id footer");
+ gApi.changes().id(r.getChangeId()).setMessage("modified commit\n");
+ }
+
+ @Test
+ public void changeCommitMessageWithWrongChangeIdFails() throws Exception {
+ PushOneCommit.Result otherChange = createChange();
+ PushOneCommit.Result r = createChange();
+ assertThat(getCommitMessage(r.getChangeId()))
+ .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("wrong Change-Id footer");
+ gApi.changes()
+ .id(r.getChangeId())
+ .setMessage("modified commit\n\nChange-Id: " + otherChange.getChangeId() + "\n");
+ }
+
+ @Test
+ public void changeCommitMessageWithoutPermissionFails() throws Exception {
+ // Create new project with clean permissions
+ Project.NameKey p = createProject("addPatchSetEdit");
+ TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
+ // Block default permission
+ block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS);
+ // Create change as user
+ PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ // Try to change the commit message
+ exception.expect(AuthException.class);
+ exception.expectMessage("modifying commit message not permitted");
+ gApi.changes().id(r.getChangeId()).setMessage("foo");
+ }
+
private static void assertCherryPickResult(
ChangeInfo changeInfo, CherryPickInput input, String srcChangeId) throws Exception {
assertThat(changeInfo.changeId).isEqualTo(srcChangeId);
@@ -1279,6 +1364,10 @@
return li.all.stream().filter(a -> a._accountId == accountId).findFirst().get();
}
+ private String getCommitMessage(String changeId) throws RestApiException, IOException {
+ return gApi.changes().id(changeId).current().file("/COMMIT_MSG").content().asString();
+ }
+
private static Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
return Iterables.transform(r, a -> new Account.Id(a._accountId));
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 6941765..d64d67f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -16,20 +16,25 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.GitUtil.getChangeId;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
import com.google.gerrit.testutil.ConfigSuite;
+import java.util.ArrayDeque;
import java.util.Map;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;
@@ -740,4 +745,69 @@
expectToHaveSubmoduleState(repoA, "master", "project-b", repoB, "master");
expectToHaveSubmoduleState(repoA, "dev", "project-b", repoB, "dev");
}
+
+ @Test
+ public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
+ assume().that(notesMigration.fuseUpdates()).isTrue();
+
+ TestRepository<?> superRepo = createProjectWithPush("super-project");
+ TestRepository<?> sub1 = createProjectWithPush("sub1");
+ TestRepository<?> sub2 = createProjectWithPush("sub2");
+
+ allowMatchingSubmoduleSubscription(
+ "sub1", "refs/heads/master", "super-project", "refs/heads/master");
+ allowMatchingSubmoduleSubscription(
+ "sub2", "refs/heads/master", "super-project", "refs/heads/master");
+
+ Config config = new Config();
+ prepareSubmoduleConfigEntry(config, "sub1", "master");
+ prepareSubmoduleConfigEntry(config, "sub2", "master");
+ pushSubmoduleConfig(superRepo, "master", config);
+
+ ObjectId superPreviousId = pushChangeTo(superRepo, "master");
+
+ String topic = "same-topic";
+ ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", topic);
+ ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master", "some message", topic);
+
+ String changeId1 = getChangeId(sub1, sub1Id).get();
+ String changeId2 = getChangeId(sub2, sub2Id).get();
+ approve(changeId1);
+ approve(changeId2);
+
+ TestSubmitInput input = new TestSubmitInput();
+ input.generateLockFailures =
+ new ArrayDeque<>(
+ ImmutableList.of(
+ false, // Change 1, attempt 1: success
+ true, // Change 2, attempt 1: lock failure
+ false, // Change 1, attempt 2: success
+ false, // Change 2, attempt 2: success
+ false)); // Leftover value to check total number of calls.
+ gApi.changes().id(changeId1).current().submit(input);
+
+ assertThat(info(changeId1).status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(info(changeId2).status).isEqualTo(ChangeStatus.MERGED);
+
+ sub1.git().fetch().call();
+ RevWalk rw1 = sub1.getRevWalk();
+ RevCommit master1 = rw1.parseCommit(getRemoteHead(name("sub1"), "master"));
+ RevCommit change1Ps = parseCurrentRevision(rw1, changeId1);
+ assertThat(rw1.isMergedInto(change1Ps, master1)).isTrue();
+
+ sub2.git().fetch().call();
+ RevWalk rw2 = sub2.getRevWalk();
+ RevCommit master2 = rw2.parseCommit(getRemoteHead(name("sub2"), "master"));
+ RevCommit change2Ps = parseCurrentRevision(rw2, changeId2);
+ assertThat(rw2.isMergedInto(change2Ps, master2)).isTrue();
+
+ assertThat(input.generateLockFailures).containsExactly(false);
+
+ expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1, "master");
+ expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
+
+ assertWithMessage("submodule subscription update should have made one commit")
+ .that(superRepo.getRepository().resolve("origin/master^"))
+ .isEqualTo(superPreviousId);
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index e4b5ff5..4334555 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -67,6 +67,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -81,6 +82,7 @@
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -482,6 +484,43 @@
}
@Test
+ public void submitReusingOldTopic() throws Exception {
+ assume().that(isSubmitWholeTopicEnabled()).isTrue();
+
+ String topic = "test-topic";
+ PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic);
+ PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "content", topic);
+ String id1 = change1.getChangeId();
+ String id2 = change2.getChangeId();
+ approve(id1);
+ approve(id2);
+ assertSubmittedTogether(id1, ImmutableList.of(id1, id2));
+ assertSubmittedTogether(id2, ImmutableList.of(id1, id2));
+ submit(id2);
+
+ String expectedTopic = name(topic);
+ change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
+ change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
+ assertSubmittedTogether(id1, ImmutableList.of(id1, id2));
+ assertSubmittedTogether(id2, ImmutableList.of(id1, id2));
+
+ PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "content", topic);
+ String id3 = change3.getChangeId();
+ approve(id3);
+ assertSubmittedTogether(id3, ImmutableList.of());
+ submit(id3);
+
+ change3.assertChange(Change.Status.MERGED, expectedTopic, admin);
+ assertSubmittedTogether(id3, ImmutableList.of());
+ }
+
+ private void assertSubmittedTogether(String changeId, Iterable<String> expected)
+ throws Exception {
+ assertThat(gApi.changes().id(changeId).submittedTogether().stream().map(i -> i.changeId))
+ .containsExactlyElementsIn(expected);
+ }
+
+ @Test
public void submitDraftChange() throws Exception {
PushOneCommit.Result draft = createDraftChange();
Change.Id num = draft.getChange().getId();
@@ -875,6 +914,81 @@
assertThat(rw.isMergedInto(fix, master)).isTrue();
}
+ @Test
+ public void retrySubmitSingleChangeOnLockFailure() throws Exception {
+ assume().that(notesMigration.fuseUpdates()).isTrue();
+
+ PushOneCommit.Result change = createChange();
+ String id = change.getChangeId();
+ approve(id);
+
+ TestSubmitInput input = new TestSubmitInput();
+ input.generateLockFailures =
+ new ArrayDeque<>(
+ ImmutableList.of(
+ true, // Attempt 1: lock failure
+ false, // Attempt 2: success
+ false)); // Leftover value to check total number of calls.
+ submit(id, input);
+ assertMerged(id);
+
+ testRepo.git().fetch().call();
+ RevWalk rw = testRepo.getRevWalk();
+ RevCommit master = rw.parseCommit(getRemoteHead(project, "master"));
+ RevCommit patchSet = parseCurrentRevision(rw, change);
+ assertThat(rw.isMergedInto(patchSet, master)).isTrue();
+
+ assertThat(input.generateLockFailures).containsExactly(false);
+ }
+
+ @Test
+ public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
+ assume().that(notesMigration.fuseUpdates()).isTrue();
+ assume().that(isSubmitWholeTopicEnabled()).isTrue();
+
+ String topic = "test-topic";
+
+ TestRepository<?> repoA = createProjectWithPush("project-a", null, getSubmitType());
+ TestRepository<?> repoB = createProjectWithPush("project-b", null, getSubmitType());
+
+ PushOneCommit.Result change1 =
+ createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
+ PushOneCommit.Result change2 =
+ createChange(repoB, "master", "Change 2", "b.txt", "content", topic);
+
+ approve(change1.getChangeId());
+ approve(change2.getChangeId());
+
+ TestSubmitInput input = new TestSubmitInput();
+ input.generateLockFailures =
+ new ArrayDeque<>(
+ ImmutableList.of(
+ false, // Change 1, attempt 1: success
+ true, // Change 2, attempt 1: lock failure
+ false, // Change 1, attempt 2: success
+ false, // Change 2, attempt 2: success
+ false)); // Leftover value to check total number of calls.
+ submit(change2.getChangeId(), input);
+
+ String expectedTopic = name(topic);
+ change1.assertChange(Change.Status.MERGED, expectedTopic, admin);
+ change2.assertChange(Change.Status.MERGED, expectedTopic, admin);
+
+ repoA.git().fetch().call();
+ RevWalk rwA = repoA.getRevWalk();
+ RevCommit masterA = rwA.parseCommit(getRemoteHead(name("project-a"), "master"));
+ RevCommit change1Ps = parseCurrentRevision(rwA, change1);
+ assertThat(rwA.isMergedInto(change1Ps, masterA)).isTrue();
+
+ repoB.git().fetch().call();
+ RevWalk rwB = repoB.getRevWalk();
+ RevCommit masterB = rwB.parseCommit(getRemoteHead(name("project-b"), "master"));
+ RevCommit change2Ps = parseCurrentRevision(rwB, change2);
+ assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue();
+
+ assertThat(input.generateLockFailures).containsExactly(false);
+ }
+
private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception {
for (PushOneCommit.Result change : changes) {
try (BatchUpdate bu =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index 48499f8..b4d8557 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -138,10 +138,11 @@
testRepo.reset(initialHead);
PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
Change.Id id2 = change2.getChange().getId();
- SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true);
+ TestSubmitInput failInput = new TestSubmitInput();
+ failInput.failAfterRefUpdates = true;
submit(
change2.getChangeId(),
- failAfterRefUpdates,
+ failInput,
ResourceConflictException.class,
"Failing after ref updates");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index b94b062..5dfc76d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -26,7 +26,6 @@
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.SubmitType;
@@ -256,10 +255,11 @@
testRepo.reset(initialHead);
PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
Change.Id id2 = change2.getChange().getId();
- SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true);
+ TestSubmitInput failInput = new TestSubmitInput();
+ failInput.failAfterRefUpdates = true;
submit(
change2.getChangeId(),
- failAfterRefUpdates,
+ failInput,
ResourceConflictException.class,
"Failing after ref updates");
RevCommit headAfterFailedSubmit = getRemoteHead();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index 5235b14..a385932 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -400,10 +400,11 @@
testRepo.reset(initialHead);
PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
Change.Id id2 = change2.getChange().getId();
- SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true);
+ TestSubmitInput failInput = new TestSubmitInput();
+ failInput.failAfterRefUpdates = true;
submit(
change2.getChangeId(),
- failAfterRefUpdates,
+ failInput,
ResourceConflictException.class,
"Failing after ref updates");
RevCommit headAfterFailedSubmit = getRemoteHead();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index a65ba82..d4397d64 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -22,7 +22,6 @@
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
@@ -151,10 +150,11 @@
PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
Change.Id id = change.getChange().getId();
- SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true);
+ TestSubmitInput failInput = new TestSubmitInput();
+ failInput.failAfterRefUpdates = true;
submit(
change.getChangeId(),
- failAfterRefUpdates,
+ failInput,
ResourceConflictException.class,
"Failing after ref updates");
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 3dabced..d52cd98 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -190,6 +190,9 @@
*/
ChangeEditApi edit() throws RestApiException;
+ /** Create a new patch set with a new commit message. */
+ void setMessage(String message) throws RestApiException;
+
/** Set hashtags on a change */
void setHashtags(HashtagsInput input) throws RestApiException;
@@ -431,6 +434,11 @@
}
@Override
+ public void setMessage(String message) {
+ throw new NotImplementedException();
+ }
+
+ @Override
public EditInfo getEdit() {
throw new NotImplementedException();
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 93673bc..51340ae 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -133,7 +133,7 @@
String src = "gerrit_ui/gerrit_ui.nocache.js";
try (InputStream in = servletContext.getResourceAsStream("/" + src)) {
if (in != null) {
- Hasher md = Hashing.md5().newHasher();
+ Hasher md = Hashing.murmur3_128().newHasher();
byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) > 0) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 914c830..150acc6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -312,7 +312,7 @@
this.lastModified = checkNotNull(lastModified, "lastModified");
this.contentType = checkNotNull(contentType, "contentType");
this.raw = checkNotNull(raw, "raw");
- this.etag = Hashing.md5().hashBytes(raw).toString();
+ this.etag = Hashing.murmur3_128().hashBytes(raw).toString();
}
boolean isStale(Path p, ResourceServlet rs) throws IOException {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index fb4e0eb..e0589ed 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -280,7 +280,7 @@
System.err.flush();
return;
}
- Hasher h = Hashing.sha1().newHasher();
+ Hasher h = Hashing.murmur3_128().newHasher();
try (InputStream in = Files.newInputStream(dst);
OutputStream out = Funnels.asOutputStream(h)) {
ByteStreams.copy(in, out);
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD
index d17bda6..cc01802 100644
--- a/gerrit-plugin-api/BUILD
+++ b/gerrit-plugin-api/BUILD
@@ -43,6 +43,7 @@
"//lib:args4j",
"//lib:blame-cache",
"//lib:guava",
+ "//lib:guava-retrying",
"//lib:gson",
"//lib:gwtorm",
"//lib:icu4j",
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java
index 9791572..d4c6354 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java
@@ -14,7 +14,6 @@
package com.google.gerrit.reviewdb.server;
-import com.google.common.util.concurrent.CheckedFuture;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -82,8 +81,10 @@
throw new UnsupportedOperationException(MSG);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<Change, OrmException> getAsync(Change.Id key) {
+ public com.google.common.util.concurrent.CheckedFuture<Change, OrmException> getAsync(
+ Change.Id key) {
throw new UnsupportedOperationException(MSG);
}
@@ -113,8 +114,10 @@
throw new UnsupportedOperationException(MSG);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<PatchSetApproval, OrmException> getAsync(PatchSetApproval.Key key) {
+ public com.google.common.util.concurrent.CheckedFuture<PatchSetApproval, OrmException> getAsync(
+ PatchSetApproval.Key key) {
throw new UnsupportedOperationException(MSG);
}
@@ -149,8 +152,10 @@
throw new UnsupportedOperationException(MSG);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<ChangeMessage, OrmException> getAsync(ChangeMessage.Key key) {
+ public com.google.common.util.concurrent.CheckedFuture<ChangeMessage, OrmException> getAsync(
+ ChangeMessage.Key key) {
throw new UnsupportedOperationException(MSG);
}
@@ -190,8 +195,10 @@
throw new UnsupportedOperationException(MSG);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<PatchSet, OrmException> getAsync(PatchSet.Id key) {
+ public com.google.common.util.concurrent.CheckedFuture<PatchSet, OrmException> getAsync(
+ PatchSet.Id key) {
throw new UnsupportedOperationException(MSG);
}
@@ -221,8 +228,10 @@
throw new UnsupportedOperationException(MSG);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<PatchLineComment, OrmException> getAsync(PatchLineComment.Key key) {
+ public com.google.common.util.concurrent.CheckedFuture<PatchLineComment, OrmException> getAsync(
+ PatchLineComment.Key key) {
throw new UnsupportedOperationException(MSG);
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 3b22889..c8c4fb2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -16,7 +16,6 @@
import static com.google.common.base.Preconditions.checkNotNull;
-import com.google.common.util.concurrent.CheckedFuture;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -190,8 +189,10 @@
return delegate.toMap(c);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<Change, OrmException> getAsync(Change.Id key) {
+ public com.google.common.util.concurrent.CheckedFuture<Change, OrmException> getAsync(
+ Change.Id key) {
return delegate.getAsync(key);
}
@@ -278,8 +279,10 @@
return delegate.toMap(c);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<PatchSetApproval, OrmException> getAsync(PatchSetApproval.Key key) {
+ public com.google.common.util.concurrent.CheckedFuture<PatchSetApproval, OrmException> getAsync(
+ PatchSetApproval.Key key) {
return delegate.getAsync(key);
}
@@ -384,8 +387,10 @@
return delegate.toMap(c);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<ChangeMessage, OrmException> getAsync(ChangeMessage.Key key) {
+ public com.google.common.util.concurrent.CheckedFuture<ChangeMessage, OrmException> getAsync(
+ ChangeMessage.Key key) {
return delegate.getAsync(key);
}
@@ -483,8 +488,10 @@
return delegate.toMap(c);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<PatchSet, OrmException> getAsync(PatchSet.Id key) {
+ public com.google.common.util.concurrent.CheckedFuture<PatchSet, OrmException> getAsync(
+ PatchSet.Id key) {
return delegate.getAsync(key);
}
@@ -577,8 +584,10 @@
return delegate.toMap(c);
}
+ @SuppressWarnings("deprecation")
@Override
- public CheckedFuture<PatchLineComment, OrmException> getAsync(PatchLineComment.Key key) {
+ public com.google.common.util.concurrent.CheckedFuture<PatchLineComment, OrmException> getAsync(
+ PatchLineComment.Key key) {
return delegate.getAsync(key);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 4aabf59..9c456f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -96,12 +96,13 @@
* Returns the SHA1 of the external ID that is used as note ID in the refs/meta/external-ids
* notes branch.
*/
+ @SuppressWarnings("deprecation") // Use Hashing.sha1 for compatibility.
public ObjectId sha1() {
return ObjectId.fromRaw(Hashing.sha1().hashString(get(), UTF_8).asBytes());
}
/**
- * Exports this external ID key as string with the format "scheme:id", or "id" id scheme is
+ * Exports this external ID key as string with the format "scheme:id", or "id" if scheme is
* null.
*
* <p>This string representation is used as subsection name in the Git config file that stores
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index f4ea3b0..4f4af54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -71,6 +71,7 @@
import com.google.gerrit.server.change.PostReviewers;
import com.google.gerrit.server.change.PublishDraftPatchSet;
import com.google.gerrit.server.change.PutAssignee;
+import com.google.gerrit.server.change.PutMessage;
import com.google.gerrit.server.change.PutTopic;
import com.google.gerrit.server.change.Rebase;
import com.google.gerrit.server.change.Restore;
@@ -139,6 +140,7 @@
private final Unmute unmute;
private final SetWorkInProgress setWip;
private final SetReadyForReview setReady;
+ private final PutMessage putMessage;
@Inject
ChangeApiImpl(
@@ -182,6 +184,7 @@
Unmute unmute,
SetWorkInProgress setWip,
SetReadyForReview setReady,
+ PutMessage putMessage,
@Assisted ChangeResource change) {
this.changeApi = changeApi;
this.revert = revert;
@@ -223,6 +226,7 @@
this.unmute = unmute;
this.setWip = setWip;
this.setReady = setReady;
+ this.putMessage = putMessage;
this.change = change;
}
@@ -511,6 +515,17 @@
}
@Override
+ public void setMessage(String in) throws RestApiException {
+ try {
+ PutMessage.Input input = new PutMessage.Input();
+ input.message = in;
+ putMessage.apply(change, input);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot edit commit message", e);
+ }
+ }
+
+ @Override
public ChangeInfo info() throws RestApiException {
return get(EnumSet.noneOf(ListChangesOption.class));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index 1bec3d1..f27c53b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -137,7 +137,7 @@
@Override
public String getETag() {
CurrentUser user = control.getUser();
- Hasher h = Hashing.md5().newHasher();
+ Hasher h = Hashing.murmur3_128().newHasher();
if (user.isIdentifiedUser()) {
h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index 0c2ec68..181505d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -273,7 +273,7 @@
// an attacker could upload a *.class file and have us send a ZIP
// that can be invoked through an applet tag in the victim's browser.
//
- Hasher h = Hashing.md5().newHasher();
+ Hasher h = Hashing.murmur3_128().newHasher();
byte[] buf = new byte[8];
NB.encodeInt64(buf, 0, TimeUtil.nowMs());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
index e476e73..8a6a1ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -63,7 +63,7 @@
@Override
public String getETag(RevisionResource rsrc) {
- Hasher h = Hashing.md5().newHasher();
+ Hasher h = Hashing.murmur3_128().newHasher();
CurrentUser user = rsrc.getControl().getUser();
try {
rsrc.getChangeResource().prepareETag(h, user);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index db31c17..c66469c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -94,6 +94,7 @@
put(CHANGE_KIND, "unmute").to(Unmute.class);
post(CHANGE_KIND, "wip").to(SetWorkInProgress.class);
post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
+ put(CHANGE_KIND, "message").to(PutMessage.class);
post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 40c6e06..bebccc4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -745,7 +745,7 @@
comment.key.patchSetId,
comment.lineNbr,
Side.fromShort(comment.side),
- Hashing.sha1().hashString(comment.message, UTF_8),
+ Hashing.murmur3_128().hashString(comment.message, UTF_8),
comment.range);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
index ab3478b..53f127e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -36,8 +36,6 @@
import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.UpdateException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -60,8 +58,7 @@
private static final int MAX_DEFAULT_BUNDLE_SIZE = 100 * 1024 * 1024;
private final Provider<ReviewDb> dbProvider;
- private final RetryHelper retryHelper;
- private final MergeOp.Factory mergeOpFactory;
+ private final Provider<MergeOp> mergeOpProvider;
private final AllowedFormats allowedFormats;
private int maxBundleSize;
private String format;
@@ -74,60 +71,53 @@
@Inject
PreviewSubmit(
Provider<ReviewDb> dbProvider,
- RetryHelper retryHelper,
- MergeOp.Factory mergeOpFactory,
+ Provider<MergeOp> mergeOpProvider,
AllowedFormats allowedFormats,
@GerritServerConfig Config cfg) {
this.dbProvider = dbProvider;
- this.retryHelper = retryHelper;
- this.mergeOpFactory = mergeOpFactory;
+ this.mergeOpProvider = mergeOpProvider;
this.allowedFormats = allowedFormats;
this.maxBundleSize = cfg.getInt("download", "maxBundleSize", MAX_DEFAULT_BUNDLE_SIZE);
}
@Override
- public BinaryResult apply(RevisionResource rsrc) throws UpdateException, RestApiException {
- // Shouldn't ever need to retry, since we are doing a dry-run BatchUpdate, but this is required
- // to get access to a BatchUpdate.Factory.
- return retryHelper.execute(
- (updateFactory) -> {
- if (Strings.isNullOrEmpty(format)) {
- throw new BadRequestException("format is not specified");
- }
- ArchiveFormat f = allowedFormats.extensions.get("." + format);
- if (f == null && format.equals("tgz")) {
- // Always allow tgz, even when the allowedFormats doesn't contain it.
- // Then we allow at least one format even if the list of allowed
- // formats is empty.
- f = ArchiveFormat.TGZ;
- }
- if (f == null) {
- throw new BadRequestException("unknown archive format");
- }
+ public BinaryResult apply(RevisionResource rsrc)
+ throws OrmException, RestApiException, UpdateException {
+ if (Strings.isNullOrEmpty(format)) {
+ throw new BadRequestException("format is not specified");
+ }
+ ArchiveFormat f = allowedFormats.extensions.get("." + format);
+ if (f == null && format.equals("tgz")) {
+ // Always allow tgz, even when the allowedFormats doesn't contain it.
+ // Then we allow at least one format even if the list of allowed
+ // formats is empty.
+ f = ArchiveFormat.TGZ;
+ }
+ if (f == null) {
+ throw new BadRequestException("unknown archive format");
+ }
- Change change = rsrc.getChange();
- if (!change.getStatus().isOpen()) {
- throw new PreconditionFailedException("change is " + ChangeUtil.status(change));
- }
- ChangeControl control = rsrc.getControl();
- if (!control.getUser().isIdentifiedUser()) {
- throw new MethodNotAllowedException("Anonymous users cannot submit");
- }
+ Change change = rsrc.getChange();
+ if (!change.getStatus().isOpen()) {
+ throw new PreconditionFailedException("change is " + ChangeUtil.status(change));
+ }
+ ChangeControl control = rsrc.getControl();
+ if (!control.getUser().isIdentifiedUser()) {
+ throw new MethodNotAllowedException("Anonymous users cannot submit");
+ }
- return getBundles(updateFactory, rsrc, f);
- });
+ return getBundles(rsrc, f);
}
- private BinaryResult getBundles(
- BatchUpdate.Factory updateFactory, RevisionResource rsrc, ArchiveFormat f)
- throws OrmException, RestApiException {
+ private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
+ throws OrmException, RestApiException, UpdateException {
ReviewDb db = dbProvider.get();
ChangeControl control = rsrc.getControl();
IdentifiedUser caller = control.getUser().asIdentifiedUser();
Change change = rsrc.getChange();
@SuppressWarnings("resource") // Returned BinaryResult takes ownership and handles closing.
- MergeOp op = mergeOpFactory.create(updateFactory);
+ MergeOp op = mergeOpProvider.get();
try {
op.merge(db, change, caller, false, new SubmitInput(), true);
BinaryResult bin = new SubmitPreviewResult(op, f, maxBundleSize);
@@ -135,7 +125,7 @@
.setContentType(f.getMimeType())
.setAttachmentName("submit-preview-" + change.getChangeId() + "." + format);
return bin;
- } catch (OrmException | RestApiException | RuntimeException e) {
+ } catch (OrmException | RestApiException | UpdateException | RuntimeException e) {
op.close();
throw e;
}
@@ -162,8 +152,7 @@
BundleWriter bw = new BundleWriter(or.getCodeReviewRevWalk().getObjectReader());
bw.setObjectCountCallback(null);
bw.setPackConfig(new PackConfig(or.getRepo()));
- Collection<ReceiveCommand> refs =
- or.getUpdate(mergeOp.getBatchUpdateFactory()).getRefUpdates().values();
+ Collection<ReceiveCommand> refs = or.getUpdate().getRefUpdates().values();
for (ReceiveCommand r : refs) {
bw.include(r.getRefName(), r.getNewId());
ObjectId oldId = r.getOldId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
new file mode 100644
index 0000000..0935c71
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
@@ -0,0 +1,217 @@
+// Copyright (C) 2017 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.change;
+
+import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.edit.UnchangedCommitMessageException;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.CommitMessageUtil;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+@Singleton
+public class PutMessage
+ extends RetryingRestModifyView<ChangeResource, PutMessage.Input, Response<?>> {
+
+ public static class Input {
+ @DefaultInput public String message;
+
+ public NotifyHandling notify = NotifyHandling.ALL;
+
+ public Map<RecipientType, NotifyInfo> notifyDetails;
+ }
+
+ private final GitRepositoryManager repositoryManager;
+ private final Provider<CurrentUser> currentUserProvider;
+ private final Provider<ReviewDb> db;
+ private final TimeZone tz;
+ private final PatchSetInserter.Factory psInserterFactory;
+ private final PermissionBackend permissionBackend;
+ private final PatchSetUtil psUtil;
+ private final NotifyUtil notifyUtil;
+
+ @Inject
+ PutMessage(
+ RetryHelper retryHelper,
+ GitRepositoryManager repositoryManager,
+ Provider<CurrentUser> currentUserProvider,
+ Provider<ReviewDb> db,
+ PatchSetInserter.Factory psInserterFactory,
+ PermissionBackend permissionBackend,
+ @GerritPersonIdent PersonIdent gerritIdent,
+ PatchSetUtil psUtil,
+ NotifyUtil notifyUtil) {
+ super(retryHelper);
+ this.repositoryManager = repositoryManager;
+ this.currentUserProvider = currentUserProvider;
+ this.db = db;
+ this.psInserterFactory = psInserterFactory;
+ this.tz = gerritIdent.getTimeZone();
+ this.permissionBackend = permissionBackend;
+ this.psUtil = psUtil;
+ this.notifyUtil = notifyUtil;
+ }
+
+ @Override
+ protected Response<String> applyImpl(
+ BatchUpdate.Factory updateFactory, ChangeResource resource, Input input)
+ throws IOException, UnchangedCommitMessageException, RestApiException, UpdateException,
+ PermissionBackendException, OrmException {
+ PatchSet ps = psUtil.current(db.get(), resource.getNotes());
+ if (ps == null) {
+ throw new ResourceConflictException("current revision is missing");
+ } else if (!resource.getControl().isPatchVisible(ps, db.get())) {
+ throw new AuthException("current revision not accessible");
+ }
+
+ if (input == null) {
+ throw new BadRequestException("input cannot be null");
+ }
+ String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message);
+
+ ensureCanEditCommitMessage(resource.getControl().getNotes());
+ ensureChangeIdIsCorrect(
+ resource.getControl().getProjectControl().getProjectState().isRequireChangeID(),
+ resource.getChange().getKey().get(),
+ sanitizedCommitMessage);
+
+ try (Repository repository = repositoryManager.openRepository(resource.getProject());
+ RevWalk revWalk = new RevWalk(repository);
+ ObjectInserter objectInserter = repository.newObjectInserter()) {
+ RevCommit patchSetCommit = revWalk.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+
+ String currentCommitMessage = patchSetCommit.getFullMessage();
+ if (input.equals(currentCommitMessage)) {
+ throw new ResourceConflictException("new and existing commit message are the same");
+ }
+
+ Timestamp ts = TimeUtil.nowTs();
+ try (BatchUpdate bu =
+ updateFactory.create(
+ db.get(), resource.getChange().getProject(), currentUserProvider.get(), ts)) {
+ // Ensure that BatchUpdate will update the same repo
+ bu.setRepository(repository, new RevWalk(objectInserter.newReader()), objectInserter);
+
+ PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.getId());
+ ObjectId newCommit =
+ createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts);
+ PatchSetInserter inserter =
+ psInserterFactory.create(resource.getControl(), psId, newCommit);
+ inserter.setMessage(
+ String.format("Patch Set %s: Commit message was updated.", psId.getId()));
+ inserter.setNotify(input.notify);
+ inserter.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+ bu.addOp(resource.getChange().getId(), inserter);
+ bu.execute();
+ }
+ }
+ return Response.ok("ok");
+ }
+
+ private ObjectId createCommit(
+ ObjectInserter objectInserter,
+ RevCommit basePatchSetCommit,
+ String commitMessage,
+ Timestamp timestamp)
+ throws IOException {
+ CommitBuilder builder = new CommitBuilder();
+ builder.setTreeId(basePatchSetCommit.getTree());
+ builder.setParentIds(basePatchSetCommit.getParents());
+ builder.setAuthor(basePatchSetCommit.getAuthorIdent());
+ builder.setCommitter(
+ currentUserProvider.get().asIdentifiedUser().newCommitterIdent(timestamp, tz));
+ builder.setMessage(commitMessage);
+ ObjectId newCommitId = objectInserter.insert(builder);
+ objectInserter.flush();
+ return newCommitId;
+ }
+
+ private void ensureCanEditCommitMessage(ChangeNotes changeNotes)
+ throws AuthException, PermissionBackendException {
+ if (!currentUserProvider.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+ try {
+ permissionBackend
+ .user(currentUserProvider.get())
+ .database(db.get())
+ .change(changeNotes)
+ .check(ChangePermission.ADD_PATCH_SET);
+ } catch (AuthException denied) {
+ throw new AuthException("modifying commit message not permitted", denied);
+ }
+ }
+
+ private 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");
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index a86767a..3fb8a79 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -29,6 +29,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Branch;
@@ -45,7 +46,6 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.account.AccountsCollection;
-import com.google.gerrit.server.change.Submit.Output;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.ChangeSet;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -58,9 +58,7 @@
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.OrmRuntimeException;
import com.google.inject.Inject;
@@ -73,6 +71,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Queue;
import java.util.Set;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
@@ -84,8 +83,8 @@
import org.slf4j.LoggerFactory;
@Singleton
-public class Submit extends RetryingRestModifyView<RevisionResource, SubmitInput, Output>
- implements UiAction<RevisionResource> {
+public class Submit
+ implements RestModifyView<RevisionResource, SubmitInput>, UiAction<RevisionResource> {
private static final Logger log = LoggerFactory.getLogger(Submit.class);
private static final String DEFAULT_TOOLTIP = "Submit patch set ${patchSet} into ${branch}";
@@ -119,13 +118,14 @@
*/
@VisibleForTesting
public static class TestSubmitInput extends SubmitInput {
- public final boolean failAfterRefUpdates;
+ public boolean failAfterRefUpdates;
- public TestSubmitInput(SubmitInput base, boolean failAfterRefUpdates) {
- this.onBehalfOf = base.onBehalfOf;
- this.notify = base.notify;
- this.failAfterRefUpdates = failAfterRefUpdates;
- }
+ /**
+ * For each change being submitted, an element is removed from this queue and, if the value is
+ * true, a bogus ref update is added to the batch, in order to generate a lock failure during
+ * execution.
+ */
+ public Queue<Boolean> generateLockFailures;
}
private final Provider<ReviewDb> dbProvider;
@@ -134,7 +134,7 @@
private final ChangeData.Factory changeDataFactory;
private final ChangeMessagesUtil cmUtil;
private final ChangeNotes.Factory changeNotesFactory;
- private final MergeOp.Factory mergeOpFactory;
+ private final Provider<MergeOp> mergeOpProvider;
private final Provider<MergeSuperSet> mergeSuperSet;
private final AccountsCollection accounts;
private final String label;
@@ -152,24 +152,22 @@
Provider<ReviewDb> dbProvider,
GitRepositoryManager repoManager,
PermissionBackend permissionBackend,
- RetryHelper retryHelper,
ChangeData.Factory changeDataFactory,
ChangeMessagesUtil cmUtil,
ChangeNotes.Factory changeNotesFactory,
- MergeOp.Factory mergeOpFactory,
+ Provider<MergeOp> mergeOpProvider,
Provider<MergeSuperSet> mergeSuperSet,
AccountsCollection accounts,
@GerritServerConfig Config cfg,
Provider<InternalChangeQuery> queryProvider,
PatchSetUtil psUtil) {
- super(retryHelper);
this.dbProvider = dbProvider;
this.repoManager = repoManager;
this.permissionBackend = permissionBackend;
this.changeDataFactory = changeDataFactory;
this.cmUtil = cmUtil;
this.changeNotesFactory = changeNotesFactory;
- this.mergeOpFactory = mergeOpFactory;
+ this.mergeOpProvider = mergeOpProvider;
this.mergeSuperSet = mergeSuperSet;
this.accounts = accounts;
this.label =
@@ -202,10 +200,9 @@
}
@Override
- protected Output applyImpl(
- BatchUpdate.Factory updateFactory, RevisionResource rsrc, SubmitInput input)
+ public Output apply(RevisionResource rsrc, SubmitInput input)
throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
- PermissionBackendException {
+ PermissionBackendException, UpdateException {
input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
IdentifiedUser submitter;
if (input.onBehalfOf != null) {
@@ -215,15 +212,11 @@
submitter = rsrc.getUser().asIdentifiedUser();
}
- return new Output(mergeChange(updateFactory, rsrc, submitter, input));
+ return new Output(mergeChange(rsrc, submitter, input));
}
- public Change mergeChange(
- BatchUpdate.Factory updateFactory,
- RevisionResource rsrc,
- IdentifiedUser submitter,
- SubmitInput input)
- throws OrmException, RestApiException, IOException {
+ public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
+ throws OrmException, RestApiException, IOException, UpdateException {
Change change = rsrc.getChange();
if (!change.getStatus().isOpen()) {
throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -237,7 +230,7 @@
"revision %s is not current revision", rsrc.getPatchSet().getRevision().get()));
}
- try (MergeOp op = mergeOpFactory.create(updateFactory)) {
+ try (MergeOp op = mergeOpProvider.get()) {
ReviewDb db = dbProvider.get();
op.merge(db, change, submitter, true, input, false);
try {
@@ -291,7 +284,7 @@
if (c.change().isWorkInProgress()) {
return BLOCKED_WORK_IN_PROGRESS;
}
- MergeOp.checkSubmitRule(c);
+ MergeOp.checkSubmitRule(c, false);
}
Collection<ChangeData> unmergeable = unmergeableChanges(cs);
@@ -328,7 +321,7 @@
&& resource.isCurrent()
&& !resource.getPatchSet().isDraft()
&& resource.permissions().test(ChangePermission.SUBMIT);
- MergeOp.checkSubmitRule(cd);
+ MergeOp.checkSubmitRule(cd, false);
} catch (ResourceConflictException e) {
visible = false;
} catch (PermissionBackendException e) {
@@ -513,8 +506,7 @@
}
}
- public static class CurrentRevision
- extends RetryingRestModifyView<ChangeResource, SubmitInput, ChangeInfo> {
+ public static class CurrentRevision implements RestModifyView<ChangeResource, SubmitInput> {
private final Provider<ReviewDb> dbProvider;
private final Submit submit;
private final ChangeJson.Factory json;
@@ -523,11 +515,9 @@
@Inject
CurrentRevision(
Provider<ReviewDb> dbProvider,
- RetryHelper retryHelper,
Submit submit,
ChangeJson.Factory json,
PatchSetUtil psUtil) {
- super(retryHelper);
this.dbProvider = dbProvider;
this.submit = submit;
this.json = json;
@@ -535,10 +525,9 @@
}
@Override
- protected ChangeInfo applyImpl(
- BatchUpdate.Factory updateFactory, ChangeResource rsrc, SubmitInput input)
+ public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
- PermissionBackendException {
+ PermissionBackendException, UpdateException {
PatchSet ps = psUtil.current(dbProvider.get(), rsrc.getNotes());
if (ps == null) {
throw new ResourceConflictException("current revision is missing");
@@ -546,7 +535,7 @@
throw new AuthException("current revision not accessible");
}
- Output out = submit.applyImpl(updateFactory, new RevisionResource(rsrc, ps), input);
+ Output out = submit.apply(new RevisionResource(rsrc, ps), input);
return json.noOptions().format(out.change);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 366cbd1..f827bd5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -115,7 +115,6 @@
import com.google.gerrit.server.git.EmailMerge;
import com.google.gerrit.server.git.GitModule;
import com.google.gerrit.server.git.GitModules;
-import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.MergedByPushOp;
import com.google.gerrit.server.git.NotesBranchUtil;
@@ -401,7 +400,6 @@
factory(MergedByPushOp.Factory.class);
factory(GitModules.Factory.class);
factory(VersionedAuthorizedKeys.Factory.class);
- factory(MergeOp.Factory.class);
bind(AccountManager.class);
factory(ChangeUserName.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 8d3cbae..79dc9c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -14,12 +14,10 @@
package com.google.gerrit.server.edit;
-import static com.google.common.base.Preconditions.checkState;
-
-import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.reviewdb.client.Change;
@@ -42,6 +40,7 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.util.CommitMessageUtil;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -205,13 +204,14 @@
* @throws AuthException if the user isn't authenticated or not allowed to use change edits
* @throws UnchangedCommitMessageException if the commit message is the same as before
* @throws PermissionBackendException
+ * @throws BadRequestException if the commit message is malformed
*/
public void modifyMessage(
Repository repository, ChangeControl changeControl, String newCommitMessage)
throws AuthException, IOException, UnchangedCommitMessageException, OrmException,
- PermissionBackendException {
+ PermissionBackendException, BadRequestException {
assertCanEdit(changeControl);
- newCommitMessage = getWellFormedCommitMessage(newCommitMessage);
+ newCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(newCommitMessage);
Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl);
@@ -435,13 +435,6 @@
}
}
- private static String getWellFormedCommitMessage(String commitMessage) {
- String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim();
- checkState(!wellFormedMessage.isEmpty(), "Commit message cannot be null or empty");
- wellFormedMessage = wellFormedMessage + "\n";
- return wellFormedMessage;
- }
-
private Optional<ChangeEdit> lookupChangeEdit(ChangeControl changeControl)
throws AuthException, IOException {
return changeEditUtil.byChange(changeControl);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
index 29570a7..322d158 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -30,7 +30,6 @@
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
-import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
@@ -87,7 +86,7 @@
public BanCommitResult ban(
ProjectControl projectControl, List<ObjectId> commitsToBan, String reason)
- throws PermissionDeniedException, IOException, ConcurrentRefUpdateException {
+ throws PermissionDeniedException, LockFailureException, IOException {
if (!projectControl.isOwner()) {
throw new PermissionDeniedException("Not project owner: not permitted to ban commits");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
index 03d44ca..d4fa9ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
@@ -22,6 +22,7 @@
import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import java.util.Collection;
@@ -91,6 +92,14 @@
return changeData.values();
}
+ public ImmutableSet<Project.NameKey> projects() throws OrmException {
+ ImmutableSet.Builder<Project.NameKey> ret = ImmutableSet.builder();
+ for (ChangeData cd : changeData.values()) {
+ ret.add(cd.project());
+ }
+ return ret.build();
+ }
+
public ImmutableSet<Change.Id> nonVisibleIds() {
return nonVisibleChanges.keySet();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 28cdebf..ce6b5c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -20,6 +20,8 @@
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toSet;
+import com.github.rholder.retry.Attempt;
+import com.github.rholder.retry.RetryListener;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableListMultimap;
@@ -69,12 +71,13 @@
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.util.RequestId;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
-import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
@@ -108,18 +111,17 @@
private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS = SubmitRuleOptions.defaults().build();
-
- public interface Factory {
- MergeOp create(BatchUpdate.Factory batchUpdateFactory);
- }
+ private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_ALLOW_CLOSED =
+ SUBMIT_RULE_OPTIONS.toBuilder().allowClosed(true).build();
public static class CommitStatus {
private final ImmutableMap<Change.Id, ChangeData> changes;
private final ImmutableSetMultimap<Branch.NameKey, Change.Id> byBranch;
private final Map<Change.Id, CodeReviewCommit> commits;
private final ListMultimap<Change.Id, String> problems;
+ private final boolean allowClosed;
- private CommitStatus(ChangeSet cs) throws OrmException {
+ private CommitStatus(ChangeSet cs, boolean allowClosed) throws OrmException {
checkArgument(
!cs.furtherHiddenChanges(), "CommitStatus must not be called with hidden changes");
changes = cs.changesById();
@@ -130,6 +132,7 @@
byBranch = bb.build();
commits = new HashMap<>();
problems = MultimapBuilder.treeKeys(comparing(Change.Id::get)).arrayListValues(1).build();
+ this.allowClosed = allowClosed;
}
public ImmutableSet<Change.Id> getChangeIds() {
@@ -182,7 +185,7 @@
// date by this point.
ChangeData cd = checkNotNull(changes.get(id), "ChangeData for %s", id);
return checkNotNull(
- cd.getSubmitRecords(SUBMIT_RULE_OPTIONS),
+ cd.getSubmitRecords(submitRuleOptions(allowClosed)),
"getSubmitRecord only valid after submit rules are evalutated");
}
@@ -226,13 +229,15 @@
private final InternalChangeQuery internalChangeQuery;
private final SubmitStrategyFactory submitStrategyFactory;
private final SubmoduleOp.Factory subOpFactory;
- private final MergeOpRepoManager orm;
+ private final Provider<MergeOpRepoManager> ormProvider;
private final NotifyUtil notifyUtil;
+ private final RetryHelper retryHelper;
private Timestamp ts;
private RequestId submissionId;
private IdentifiedUser caller;
+ private MergeOpRepoManager orm;
private CommitStatus commitStatus;
private ReviewDb db;
private SubmitInput submitInput;
@@ -244,40 +249,45 @@
@Inject
MergeOp(
ChangeMessagesUtil cmUtil,
+ BatchUpdate.Factory batchUpdateFactory,
InternalUser.Factory internalUserFactory,
MergeSuperSet mergeSuperSet,
MergeValidators.Factory mergeValidatorsFactory,
InternalChangeQuery internalChangeQuery,
SubmitStrategyFactory submitStrategyFactory,
SubmoduleOp.Factory subOpFactory,
- MergeOpRepoManager orm,
+ Provider<MergeOpRepoManager> ormProvider,
NotifyUtil notifyUtil,
TopicMetrics topicMetrics,
- @Assisted BatchUpdate.Factory batchUpdateFactory) {
+ RetryHelper retryHelper) {
this.cmUtil = cmUtil;
+ this.batchUpdateFactory = batchUpdateFactory;
this.internalUserFactory = internalUserFactory;
this.mergeSuperSet = mergeSuperSet;
this.mergeValidatorsFactory = mergeValidatorsFactory;
this.internalChangeQuery = internalChangeQuery;
this.submitStrategyFactory = submitStrategyFactory;
this.subOpFactory = subOpFactory;
- this.orm = orm;
+ this.ormProvider = ormProvider;
this.notifyUtil = notifyUtil;
- this.batchUpdateFactory = batchUpdateFactory;
+ this.retryHelper = retryHelper;
this.topicMetrics = topicMetrics;
}
@Override
public void close() {
- orm.close();
+ if (orm != null) {
+ orm.close();
+ }
}
- public static void checkSubmitRule(ChangeData cd) throws ResourceConflictException, OrmException {
+ public static void checkSubmitRule(ChangeData cd, boolean allowClosed)
+ throws ResourceConflictException, OrmException {
PatchSet patchSet = cd.currentPatchSet();
if (patchSet == null) {
throw new ResourceConflictException("missing current patch set for change " + cd.getId());
}
- List<SubmitRecord> results = getSubmitRecords(cd);
+ List<SubmitRecord> results = getSubmitRecords(cd, allowClosed);
if (SubmitRecord.findOkRecord(results).isPresent()) {
// Rules supplied a valid solution.
return;
@@ -311,8 +321,13 @@
throw new IllegalStateException();
}
- private static List<SubmitRecord> getSubmitRecords(ChangeData cd) throws OrmException {
- return cd.submitRecords(SUBMIT_RULE_OPTIONS);
+ private static SubmitRuleOptions submitRuleOptions(boolean allowClosed) {
+ return allowClosed ? SUBMIT_RULE_OPTIONS_ALLOW_CLOSED : SUBMIT_RULE_OPTIONS;
+ }
+
+ private static List<SubmitRecord> getSubmitRecords(ChangeData cd, boolean allowClosed)
+ throws OrmException {
+ return cd.submitRecords(submitRuleOptions(allowClosed));
}
private static String describeLabels(ChangeData cd, List<SubmitRecord.Label> labels)
@@ -346,19 +361,23 @@
return Joiner.on("; ").join(labelResults);
}
- private void checkSubmitRulesAndState(ChangeSet cs) throws ResourceConflictException {
+ private void checkSubmitRulesAndState(ChangeSet cs, boolean allowMerged)
+ throws ResourceConflictException {
checkArgument(
!cs.furtherHiddenChanges(), "checkSubmitRulesAndState called for topic with hidden change");
for (ChangeData cd : cs.changes()) {
try {
- if (cd.change().getStatus() != Change.Status.NEW) {
- commitStatus.problem(
- cd.getId(),
- "Change " + cd.getId() + " is " + cd.change().getStatus().toString().toLowerCase());
+ Change.Status status = cd.change().getStatus();
+ if (status != Change.Status.NEW) {
+ if (!(status == Change.Status.MERGED && allowMerged)) {
+ commitStatus.problem(
+ cd.getId(),
+ "Change " + cd.getId() + " is " + cd.change().getStatus().toString().toLowerCase());
+ }
} else if (cd.change().isWorkInProgress()) {
commitStatus.problem(cd.getId(), "Change " + cd.getId() + " is work in progress");
} else {
- checkSubmitRule(cd);
+ checkSubmitRule(cd, allowMerged);
}
} catch (ResourceConflictException e) {
commitStatus.problem(cd.getId(), e.getMessage());
@@ -371,13 +390,13 @@
commitStatus.maybeFailVerbose();
}
- private void bypassSubmitRules(ChangeSet cs) {
+ private void bypassSubmitRules(ChangeSet cs, boolean allowClosed) {
checkArgument(
!cs.furtherHiddenChanges(), "cannot bypass submit rules for topic with hidden change");
for (ChangeData cd : cs.changes()) {
List<SubmitRecord> records;
try {
- records = new ArrayList<>(getSubmitRecords(cd));
+ records = new ArrayList<>(getSubmitRecords(cd, allowClosed));
} catch (OrmException e) {
log.warn("Error checking submit rules for change " + cd.getId(), e);
records = new ArrayList<>(1);
@@ -385,7 +404,7 @@
SubmitRecord forced = new SubmitRecord();
forced.status = SubmitRecord.Status.FORCED;
records.add(forced);
- cd.setSubmitRecords(SUBMIT_RULE_OPTIONS, records);
+ cd.setSubmitRecords(submitRuleOptions(allowClosed), records);
}
}
@@ -411,7 +430,7 @@
boolean checkSubmitRules,
SubmitInput submitInput,
boolean dryrun)
- throws OrmException, RestApiException {
+ throws OrmException, RestApiException, UpdateException {
this.submitInput = submitInput;
this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails);
this.dryrun = dryrun;
@@ -419,7 +438,7 @@
this.ts = TimeUtil.nowTs();
submissionId = RequestId.forChange(change);
this.db = db;
- orm.setContext(db, ts, caller, submissionId);
+ openRepoManager();
logDebug("Beginning integration of {}", change);
try {
@@ -430,21 +449,47 @@
throw new AuthException(
"A change to be submitted with " + change.getId() + " is not visible");
}
- this.commitStatus = new CommitStatus(cs);
- MergeSuperSet.reloadChanges(cs);
logDebug("Calculated to merge {}", cs);
- if (checkSubmitRules) {
- logDebug("Checking submit rules and state");
- checkSubmitRulesAndState(cs);
- } else {
- logDebug("Bypassing submit rules");
- bypassSubmitRules(cs);
+
+ // Count cross-project submissions outside of the retry loop. The chance of a single project
+ // failing increases with the number of projects, so the failure count would be inflated if
+ // this metric were incremented inside of integrateIntoHistory.
+ int projects = cs.projects().size();
+ if (projects > 1) {
+ topicMetrics.topicSubmissions.increment();
}
- try {
- integrateIntoHistory(cs);
- } catch (IntegrationException e) {
- logError("Error from integrateIntoHistory", e);
- throw new ResourceConflictException(e.getMessage(), e);
+
+ RetryTracker retryTracker = new RetryTracker();
+ retryHelper.execute(
+ updateFactory -> {
+ long attempt = retryTracker.lastAttemptNumber + 1;
+ boolean isRetry = attempt > 1;
+ if (isRetry) {
+ logDebug("Retrying, attempt #{}; skipping merged changes", attempt);
+ this.ts = TimeUtil.nowTs();
+ openRepoManager();
+ }
+ this.commitStatus = new CommitStatus(cs, isRetry);
+ MergeSuperSet.reloadChanges(cs);
+ if (checkSubmitRules) {
+ logDebug("Checking submit rules and state");
+ checkSubmitRulesAndState(cs, isRetry);
+ } else {
+ logDebug("Bypassing submit rules");
+ bypassSubmitRules(cs, isRetry);
+ }
+ try {
+ integrateIntoHistory(cs);
+ } catch (IntegrationException e) {
+ logError("Error from integrateIntoHistory", e);
+ throw new ResourceConflictException(e.getMessage(), e);
+ }
+ return null;
+ },
+ retryTracker);
+
+ if (projects > 1) {
+ topicMetrics.topicSubmissionsCompleted.increment();
}
} catch (IOException e) {
// Anything before the merge attempt is an error
@@ -452,6 +497,23 @@
}
}
+ private void openRepoManager() {
+ if (orm != null) {
+ orm.close();
+ }
+ orm = ormProvider.get();
+ orm.setContext(db, ts, caller, submissionId);
+ }
+
+ private class RetryTracker implements RetryListener {
+ long lastAttemptNumber;
+
+ @Override
+ public <V> void onRetry(Attempt<V> attempt) {
+ lastAttemptNumber = attempt.getAttemptNumber();
+ }
+ }
+
@Singleton
private static class TopicMetrics {
final Counter0 topicSubmissions;
@@ -471,7 +533,8 @@
}
}
- private void integrateIntoHistory(ChangeSet cs) throws IntegrationException, RestApiException {
+ private void integrateIntoHistory(ChangeSet cs)
+ throws IntegrationException, RestApiException, UpdateException {
checkArgument(!cs.furtherHiddenChanges(), "cannot integrate hidden changes into history");
logDebug("Beginning merge attempt on {}", cs);
Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>();
@@ -484,30 +547,22 @@
}
Set<Branch.NameKey> branches = cbb.keySet();
- int projects = 0;
for (Branch.NameKey branch : branches) {
OpenRepo or = openRepo(branch.getParentKey());
if (or != null) {
- BranchBatch bb = validateChangeList(or, cbb.get(branch));
- toSubmit.put(branch, bb);
- if (!bb.commits().isEmpty()) {
- projects++;
- }
+ toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
}
}
// Done checks that don't involve running submit strategies.
commitStatus.maybeFailVerbose();
- if (projects > 1) {
- topicMetrics.topicSubmissions.increment();
- }
try {
SubmoduleOp submoduleOp = subOpFactory.create(branches, orm);
List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, submoduleOp, dryrun);
this.allProjects = submoduleOp.getProjectsInOrder();
batchUpdateFactory.execute(
- orm.batchUpdates(batchUpdateFactory, allProjects),
+ orm.batchUpdates(allProjects),
new SubmitStrategyListener(submitInput, strategies, commitStatus),
submissionId,
dryrun);
@@ -516,6 +571,15 @@
} catch (IOException | SubmoduleException e) {
throw new IntegrationException(e);
} catch (UpdateException e) {
+ if (e.getCause() instanceof LockFailureException) {
+ // Lock failures are a special case: RetryHelper depends on this specific causal chain in
+ // order to trigger a retry. The downside of throwing here is we will not get the nicer
+ // error message constructed below, in the case where this is the final attempt and the
+ // operation is not retried further. This is not a huge downside, and is hopefully so rare
+ // as to be unnoticeable, assuming RetryHelper is retrying sufficiently.
+ throw e;
+ }
+
// BatchUpdate may have inadvertently wrapped an IntegrationException
// thrown by some legacy SubmitStrategyOp code that intended the error
// message to be user-visible. Copy the message from the wrapped
@@ -531,10 +595,6 @@
}
throw new IntegrationException(msg, e);
}
-
- if (projects > 1) {
- topicMetrics.topicSubmissionsCompleted.increment();
- }
}
public Set<Project.NameKey> getAllProjects() {
@@ -545,10 +605,6 @@
return orm;
}
- public BatchUpdate.Factory getBatchUpdateFactory() {
- return batchUpdateFactory;
- }
-
private List<SubmitStrategy> getSubmitStrategies(
Map<Branch.NameKey, BranchBatch> toSubmit, SubmoduleOp submoduleOp, boolean dryrun)
throws IntegrationException, NoSuchProjectException, IOException {
@@ -580,20 +636,20 @@
ob.mergeTip,
commitStatus,
submissionId,
- submitInput.notify,
+ submitInput,
accountsToNotify,
submoduleOp,
dryrun);
strategies.add(strategy);
- strategy.addOps(or.getUpdate(batchUpdateFactory), commitsToSubmit);
+ strategy.addOps(or.getUpdate(), commitsToSubmit);
if (submitting.submitType().equals(SubmitType.FAST_FORWARD_ONLY)
&& submoduleOp.hasSubscription(branch)) {
- submoduleOp.addOp(or.getUpdate(batchUpdateFactory), branch);
+ submoduleOp.addOp(or.getUpdate(), branch);
}
} else {
// no open change for this branch
// add submodule triggered op into BatchUpdate
- submoduleOp.addOp(or.getUpdate(batchUpdateFactory), branch);
+ submoduleOp.addOp(or.getUpdate(), branch);
}
}
return strategies;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index ae2c481..ad205f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -101,7 +101,7 @@
return rw;
}
- public BatchUpdate getUpdate(BatchUpdate.Factory batchUpdateFactory) {
+ public BatchUpdate getUpdate() {
checkState(db != null, "call setContext before getUpdate");
if (update == null) {
update =
@@ -149,6 +149,7 @@
}
private final Map<Project.NameKey, OpenRepo> openRepos;
+ private final BatchUpdate.Factory batchUpdateFactory;
private final OnSubmitValidators.Factory onSubmitValidatorsFactory;
private final GitRepositoryManager repoManager;
private final ProjectCache projectCache;
@@ -162,9 +163,11 @@
MergeOpRepoManager(
GitRepositoryManager repoManager,
ProjectCache projectCache,
+ BatchUpdate.Factory batchUpdateFactory,
OnSubmitValidators.Factory onSubmitValidatorsFactory) {
this.repoManager = repoManager;
this.projectCache = projectCache;
+ this.batchUpdateFactory = batchUpdateFactory;
this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;
openRepos = new HashMap<>();
@@ -199,12 +202,11 @@
}
}
- public List<BatchUpdate> batchUpdates(
- BatchUpdate.Factory batchUpdateFactory, Collection<Project.NameKey> projects)
+ public List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects)
throws NoSuchProjectException, IOException {
List<BatchUpdate> updates = new ArrayList<>(projects.size());
for (Project.NameKey project : projects) {
- updates.add(getRepo(project).getUpdate(batchUpdateFactory).setRefLogMessage("merged"));
+ updates.add(getRepo(project).getUpdate().setRefLogMessage("merged"));
}
return updates;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
index fe35231..24b3727 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -14,32 +14,29 @@
package com.google.gerrit.server.git;
+import static com.google.common.base.MoreObjects.firstNonNull;
+
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
-import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.notes.NoteMapMerger;
import org.eclipse.jgit.notes.NoteMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
/** A utility class for updating a notes branch with automatic merge of note trees. */
public class NotesBranchUtil {
@@ -47,9 +44,6 @@
NotesBranchUtil create(Project.NameKey project, Repository db, ObjectInserter inserter);
}
- private static final int MAX_LOCK_FAILURE_CALLS = 10;
- private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
-
private final PersonIdent gerritIdent;
private final GitReferenceUpdated gitRefUpdated;
private final Project.NameKey project;
@@ -86,16 +80,20 @@
* Create a new commit in the {@code notesBranch} by updating existing or creating new notes from
* the {@code notes} map.
*
+ * <p>Does not retry in the case of lock failure; callers may use {@link
+ * com.google.gerrit.server.update.RetryHelper}.
+ *
* @param notes map of notes
* @param notesBranch notes branch to update
* @param commitAuthor author of the commit in the notes branch
* @param commitMessage for the commit in the notes branch
- * @throws IOException
- * @throws ConcurrentRefUpdateException
+ * @throws LockFailureException if committing the notes failed due to a lock failure on the notes
+ * branch
+ * @throws IOException if committing the notes failed for any other reason
*/
public final void commitAllNotes(
NoteMap notes, String notesBranch, PersonIdent commitAuthor, String commitMessage)
- throws IOException, ConcurrentRefUpdateException {
+ throws IOException {
this.overwrite = true;
commitNotes(notes, notesBranch, commitAuthor, commitMessage);
}
@@ -105,17 +103,21 @@
* {@code notes} map. The notes from the {@code notes} map which already exist in the note-tree of
* the tip of the {@code notesBranch} will not be updated.
*
+ * <p>Does not retry in the case of lock failure; callers may use {@link
+ * com.google.gerrit.server.update.RetryHelper}.
+ *
* @param notes map of notes
* @param notesBranch notes branch to update
* @param commitAuthor author of the commit in the notes branch
* @param commitMessage for the commit in the notes branch
* @return map with those notes from the {@code notes} that were newly created
- * @throws IOException
- * @throws ConcurrentRefUpdateException
+ * @throws LockFailureException if committing the notes failed due to a lock failure on the notes
+ * branch
+ * @throws IOException if committing the notes failed for any other reason
*/
public final NoteMap commitNewNotes(
NoteMap notes, String notesBranch, PersonIdent commitAuthor, String commitMessage)
- throws IOException, ConcurrentRefUpdateException {
+ throws IOException {
this.overwrite = false;
commitNotes(notes, notesBranch, commitAuthor, commitMessage);
NoteMap newlyCreated = NoteMap.newEmptyMap();
@@ -129,7 +131,7 @@
private void commitNotes(
NoteMap notes, String notesBranch, PersonIdent commitAuthor, String commitMessage)
- throws IOException, ConcurrentRefUpdateException {
+ throws LockFailureException, IOException {
try {
revWalk = new RevWalk(db);
reader = db.newObjectReader();
@@ -209,61 +211,16 @@
return revWalk.parseCommit(commitId);
}
- private void updateRef(String notesBranch)
- throws IOException, MissingObjectException, IncorrectObjectTypeException,
- CorruptObjectException, ConcurrentRefUpdateException {
+ private void updateRef(String notesBranch) throws LockFailureException, IOException {
if (baseCommit != null && oursCommit.getTree().equals(baseCommit.getTree())) {
// If the trees are identical, there is no change in the notes.
// Avoid saving this commit as it has no new information.
return;
}
-
- int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
- RefUpdate refUpdate = createRefUpdate(notesBranch, oursCommit, baseCommit);
-
- for (; ; ) {
- Result result = refUpdate.update();
-
- if (result == Result.LOCK_FAILURE) {
- if (--remainingLockFailureCalls > 0) {
- try {
- Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
- } catch (InterruptedException e) {
- // ignore
- }
- } else {
- throw new ConcurrentRefUpdateException(
- "Failed to lock the ref: " + notesBranch, refUpdate.getRef(), result);
- }
-
- } else if (result == Result.REJECTED) {
- RevCommit theirsCommit = revWalk.parseCommit(refUpdate.getOldObjectId());
- NoteMap theirs = NoteMap.read(revWalk.getObjectReader(), theirsCommit);
- NoteMapMerger merger = new NoteMapMerger(db, getNoteMerger(), MergeStrategy.RESOLVE);
- NoteMap merged = merger.merge(base, ours, theirs);
- RevCommit mergeCommit =
- createCommit(merged, gerritIdent, "Merged note commits\n", theirsCommit, oursCommit);
- refUpdate = createRefUpdate(notesBranch, mergeCommit, theirsCommit);
- remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
-
- } else if (result == Result.IO_FAILURE) {
- throw new IOException("Couldn't update " + notesBranch + ". " + result.name());
- } else {
- gitRefUpdated.fire(project, refUpdate, null);
- break;
- }
- }
- }
-
- private RefUpdate createRefUpdate(
- String notesBranch, ObjectId newObjectId, ObjectId expectedOldObjectId) throws IOException {
- RefUpdate refUpdate = db.updateRef(notesBranch);
- refUpdate.setNewObjectId(newObjectId);
- if (expectedOldObjectId == null) {
- refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
- } else {
- refUpdate.setExpectedOldObjectId(expectedOldObjectId);
- }
- return refUpdate;
+ BatchRefUpdate bru = db.getRefDatabase().newBatchUpdate();
+ bru.addCommand(
+ new ReceiveCommand(firstNonNull(baseCommit, ObjectId.zeroId()), oursCommit, notesBranch));
+ RefUpdateUtil.executeChecked(bru, revWalk);
+ gitRefUpdated.fire(project, bru, null);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 585c8e2..86daf8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -346,7 +346,7 @@
private Map<String, Ref> allRefs;
private final SubmoduleOp.Factory subOpFactory;
- private final MergeOp.Factory mergeOpFactory;
+ private final Provider<MergeOp> mergeOpProvider;
private final Provider<MergeOpRepoManager> ormProvider;
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
private final NotesMigration notesMigration;
@@ -400,7 +400,7 @@
@Assisted ProjectControl projectControl,
@Assisted Repository repo,
SubmoduleOp.Factory subOpFactory,
- MergeOp.Factory mergeOpFactory,
+ Provider<MergeOp> mergeOpProvider,
Provider<MergeOpRepoManager> ormProvider,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
NotesMigration notesMigration,
@@ -448,7 +448,7 @@
this.receiveId = RequestId.forProject(project.getNameKey());
this.subOpFactory = subOpFactory;
- this.mergeOpFactory = mergeOpFactory;
+ this.mergeOpProvider = mergeOpProvider;
this.ormProvider = ormProvider;
this.pluginConfigEntries = pluginConfigEntries;
this.notesMigration = notesMigration;
@@ -657,7 +657,7 @@
try (MergeOpRepoManager orm = ormProvider.get()) {
orm.setContext(db, TimeUtil.nowTs(), user, receiveId);
SubmoduleOp op = subOpFactory.create(branches, orm);
- op.updateSuperProjects(batchUpdateFactory);
+ op.updateSuperProjects();
} catch (SubmoduleException e) {
logError("Can't update the superprojects", e);
}
@@ -815,7 +815,7 @@
} catch (ResourceConflictException e) {
addMessage(e.getMessage());
reject(magicBranchCmd, "conflict");
- } catch (RestApiException | OrmException e) {
+ } catch (RestApiException | OrmException | UpdateException e) {
logError("Error submitting changes to " + project.getName(), e);
reject(magicBranchCmd, "error during submit");
}
@@ -2261,7 +2261,7 @@
}
private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
- throws OrmException, RestApiException {
+ throws OrmException, RestApiException, UpdateException {
Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
for (CreateRequest r : create) {
checkNotNull(r.change, "cannot submit new change %s; op may not have run", r.changeId);
@@ -2275,7 +2275,7 @@
tipChange, "tip of push does not correspond to a change; found these changes: %s", bySha);
logDebug(
"Processing submit with tip change {} ({})", tipChange.getId(), magicBranch.cmd.getNewId());
- try (MergeOp op = mergeOpFactory.create(batchUpdateFactory)) {
+ try (MergeOp op = mergeOpProvider.get()) {
op.merge(db, tipChange, user, false, new SubmitInput(), false);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 69909eb..eb9c024 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -355,7 +355,7 @@
return ret;
}
- public void updateSuperProjects(BatchUpdate.Factory updateFactory) throws SubmoduleException {
+ public void updateSuperProjects() throws SubmoduleException {
ImmutableSet<Project.NameKey> projects = getProjectsInOrder();
if (projects == null) {
return;
@@ -370,15 +370,12 @@
// get a new BatchUpdate for the super project
OpenRepo or = orm.getRepo(project);
for (Branch.NameKey branch : branchesByProject.get(project)) {
- addOp(or.getUpdate(updateFactory), branch);
+ addOp(or.getUpdate(), branch);
}
}
}
batchUpdateFactory.execute(
- orm.batchUpdates(updateFactory, superProjects),
- BatchUpdateListener.NONE,
- orm.getSubmissionId(),
- false);
+ orm.batchUpdates(superProjects), BatchUpdateListener.NONE, orm.getSubmissionId(), false);
} catch (RestApiException | UpdateException | IOException | NoSuchProjectException e) {
throw new SubmoduleException("Cannot update gitlinks", e);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index 95bdf33..9892b6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -18,12 +18,13 @@
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
@@ -32,6 +33,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
import com.google.gerrit.server.extensions.events.ChangeMerged;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -96,7 +98,7 @@
Set<RevCommit> alreadyAccepted,
Set<CodeReviewCommit> incoming,
RequestId submissionId,
- NotifyHandling notifyHandling,
+ SubmitInput submitInput,
ListMultimap<RecipientType, Account.Id> accountsToNotify,
SubmoduleOp submoduleOp,
boolean dryrun);
@@ -129,7 +131,7 @@
final Set<RevCommit> alreadyAccepted;
final RequestId submissionId;
final SubmitType submitType;
- final NotifyHandling notifyHandling;
+ final SubmitInput submitInput;
final ListMultimap<RecipientType, Account.Id> accountsToNotify;
final SubmoduleOp submoduleOp;
@@ -169,7 +171,7 @@
@Assisted Set<CodeReviewCommit> incoming,
@Assisted RequestId submissionId,
@Assisted SubmitType submitType,
- @Assisted NotifyHandling notifyHandling,
+ @Assisted SubmitInput submitInput,
@Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify,
@Assisted SubmoduleOp submoduleOp,
@Assisted boolean dryrun) {
@@ -199,7 +201,7 @@
this.alreadyAccepted = alreadyAccepted;
this.submissionId = submissionId;
this.submitType = submitType;
- this.notifyHandling = notifyHandling;
+ this.submitInput = submitInput;
this.accountsToNotify = accountsToNotify;
this.submoduleOp = submoduleOp;
this.dryrun = dryrun;
@@ -255,12 +257,21 @@
List<CodeReviewCommit> difference = new ArrayList<>(Sets.difference(toMerge, added));
Collections.reverse(difference);
for (CodeReviewCommit c : difference) {
- bu.addOp(c.change().getId(), new ImplicitIntegrateOp(args, c));
+ Change.Id id = c.change().getId();
+ bu.addOp(id, new ImplicitIntegrateOp(args, c));
+ maybeAddTestHelperOp(bu, id);
}
// Then ops for explicitly merged changes
for (SubmitStrategyOp op : ops) {
bu.addOp(op.getId(), op);
+ maybeAddTestHelperOp(bu, op.getId());
+ }
+ }
+
+ private void maybeAddTestHelperOp(BatchUpdate bu, Change.Id changeId) {
+ if (args.submitInput instanceof TestSubmitInput) {
+ bu.addOp(changeId, new TestHelperOp(changeId, args));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index 8f43a49..7678623 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -15,8 +15,8 @@
package com.google.gerrit.server.git.strategy;
import com.google.common.collect.ListMultimap;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
@@ -61,7 +61,7 @@
MergeTip mergeTip,
CommitStatus commitStatus,
RequestId submissionId,
- NotifyHandling notifyHandling,
+ SubmitInput submitInput,
ListMultimap<RecipientType, Account.Id> accountsToNotify,
SubmoduleOp submoduleOp,
boolean dryrun)
@@ -79,7 +79,7 @@
alreadyAccepted,
incoming,
submissionId,
- notifyHandling,
+ submitInput,
accountsToNotify,
submoduleOp,
dryrun);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index 6bf8b2e..aaad040 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -77,7 +77,8 @@
private ObjectId mergeResultRev;
private PatchSet mergedPatchSet;
private Change updatedChange;
- private CodeReviewCommit alreadyMerged;
+ private CodeReviewCommit alreadyMergedCommit;
+ private boolean changeAlreadyMerged;
protected SubmitStrategyOp(SubmitStrategy.Arguments args, CodeReviewCommit toMerge) {
this.args = args;
@@ -112,11 +113,11 @@
// Run the submit strategy implementation and record the merge tip state so
// we can create the ref update.
CodeReviewCommit tipBefore = args.mergeTip.getCurrentTip();
- alreadyMerged = getAlreadyMergedCommit(ctx);
- if (alreadyMerged == null) {
+ alreadyMergedCommit = getAlreadyMergedCommit(ctx);
+ if (alreadyMergedCommit == null) {
updateRepoImpl(ctx);
} else {
- logDebug("Already merged as {}", alreadyMerged.name());
+ logDebug("Already merged as {}", alreadyMergedCommit.name());
}
CodeReviewCommit tipAfter = args.mergeTip.getCurrentTip();
@@ -219,8 +220,23 @@
PatchSet.Id oldPsId = checkNotNull(toMerge.getPatchsetId());
PatchSet.Id newPsId;
- if (alreadyMerged != null) {
- alreadyMerged.setControl(ctx.getControl());
+ if (ctx.getChange().getStatus() == Change.Status.MERGED) {
+ // Either another thread won a race, or we are retrying a whole topic submission after one
+ // repo failed with lock failure.
+ if (alreadyMergedCommit == null) {
+ logDebug(
+ "Change is already merged according to its status, but we were unable to find it"
+ + " merged into the current tip ({})",
+ args.mergeTip.getCurrentTip().name());
+ } else {
+ logDebug("Change is already merged");
+ }
+ changeAlreadyMerged = true;
+ return false;
+ }
+
+ if (alreadyMergedCommit != null) {
+ alreadyMergedCommit.setControl(ctx.getControl());
mergedPatchSet = getOrCreateAlreadyMergedPatchSet(ctx);
newPsId = mergedPatchSet.getId();
} else {
@@ -263,12 +279,12 @@
setApproval(ctx, args.caller);
mergeResultRev =
- alreadyMerged == null
+ alreadyMergedCommit == null
? args.mergeTip.getMergeResults().get(commit)
// Our fixup code is not smart enough to find a merge commit
// corresponding to the merge result. This results in a different
// ChangeMergedEvent in the fixup case, but we'll just live with that.
- : alreadyMerged;
+ : alreadyMergedCommit;
try {
setMerged(ctx, message(ctx, commit, s));
} catch (OrmException err) {
@@ -284,13 +300,13 @@
private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx)
throws IOException, OrmException {
- PatchSet.Id psId = alreadyMerged.getPatchsetId();
+ PatchSet.Id psId = alreadyMergedCommit.getPatchsetId();
logDebug("Fixing up already-merged patch set {}", psId);
PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes());
- ctx.getRevWalk().parseBody(alreadyMerged);
+ ctx.getRevWalk().parseBody(alreadyMergedCommit);
ctx.getChange()
.setCurrentPatchSet(
- psId, alreadyMerged.getShortMessage(), ctx.getChange().getOriginalSubject());
+ psId, alreadyMergedCommit.getShortMessage(), ctx.getChange().getOriginalSubject());
PatchSet existing = args.psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
if (existing != null) {
logDebug("Patch set row exists, only updating change");
@@ -300,13 +316,13 @@
// a patch set ref. Fix up the database. Note that this uses the current
// user as the uploader, which is as good a guess as any.
List<String> groups =
- prevPs != null ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMerged);
+ prevPs != null ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMergedCommit);
return args.psUtil.insert(
ctx.getDb(),
ctx.getRevWalk(),
ctx.getUpdate(psId),
psId,
- alreadyMerged,
+ alreadyMergedCommit,
false,
groups,
null,
@@ -482,6 +498,18 @@
@Override
public final void postUpdate(Context ctx) throws Exception {
+ if (changeAlreadyMerged) {
+ // TODO(dborowitz): This is suboptimal behavior in the presence of retries: postUpdate steps
+ // will never get run for changes that submitted successfully on any but the final attempt.
+ // This is primarily a temporary workaround for the fact that the submitter field is not
+ // populated in the changeAlreadyMerged case.
+ //
+ // If we naively execute postUpdate even if the change is already merged when updateChange
+ // being, then we are subject to a race where postUpdate steps are run twice if two submit
+ // processes run at the same time.
+ logDebug("Skipping post-update steps for change {}", getId());
+ return;
+ }
postUpdateImpl(ctx);
if (command != null) {
@@ -508,7 +536,7 @@
ctx.getProject(),
getId(),
submitter.getAccountId(),
- args.notifyHandling,
+ args.submitInput.notify,
args.accountsToNotify)
.sendAsync();
} catch (Exception e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/TestHelperOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/TestHelperOp.java
new file mode 100644
index 0000000..8d95045
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/TestHelperOp.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2017 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.git.strategy;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.change.Submit.TestSubmitInput;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.RepoContext;
+import com.google.gerrit.server.util.RequestId;
+import java.io.IOException;
+import java.util.Queue;
+import org.eclipse.jgit.lib.ObjectId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class TestHelperOp implements BatchUpdateOp {
+ private static final Logger log = LoggerFactory.getLogger(TestHelperOp.class);
+
+ private final Change.Id changeId;
+ private final TestSubmitInput input;
+ private final RequestId submissionId;
+
+ TestHelperOp(Change.Id changeId, SubmitStrategy.Arguments args) {
+ this.changeId = changeId;
+ this.input = (TestSubmitInput) args.submitInput;
+ this.submissionId = args.submissionId;
+ }
+
+ @Override
+ public void updateRepo(RepoContext ctx) throws IOException {
+ Queue<Boolean> q = input.generateLockFailures;
+ if (q != null && !q.isEmpty() && q.remove()) {
+ logDebug("Adding bogus ref update to trigger lock failure, via change {}", changeId);
+ ctx.addRefUpdate(
+ ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
+ ObjectId.zeroId(),
+ "refs/test/" + getClass().getSimpleName());
+ }
+ }
+
+ private void logDebug(String msg, Object... args) {
+ if (log.isDebugEnabled()) {
+ log.debug(submissionId + msg, args);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 4edfab2..431ac7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -19,7 +19,6 @@
import com.google.common.base.Function;
import com.google.common.util.concurrent.Atomics;
-import com.google.common.util.concurrent.CheckedFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
@@ -73,7 +72,8 @@
ChangeIndexer create(ListeningExecutorService executor, ChangeIndexCollection indexes);
}
- public static CheckedFuture<?, IOException> allAsList(
+ @SuppressWarnings("deprecation")
+ public static com.google.common.util.concurrent.CheckedFuture<?, IOException> allAsList(
List<? extends ListenableFuture<?>> futures) {
// allAsList propagates the first seen exception, wrapped in
// ExecutionException, so we can reuse the same mapper as for a single
@@ -173,7 +173,9 @@
* @param id change to index.
* @return future for the indexing task.
*/
- public CheckedFuture<?, IOException> indexAsync(Project.NameKey project, Change.Id id) {
+ @SuppressWarnings("deprecation")
+ public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync(
+ Project.NameKey project, Change.Id id) {
return submit(new IndexTask(project, id));
}
@@ -183,7 +185,8 @@
* @param ids changes to index.
* @return future for completing indexing of all changes.
*/
- public CheckedFuture<?, IOException> indexAsync(
+ @SuppressWarnings("deprecation")
+ public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync(
Project.NameKey project, Collection<Change.Id> ids) {
List<ListenableFuture<?>> futures = new ArrayList<>(ids.size());
for (Change.Id id : ids) {
@@ -277,7 +280,8 @@
* @param id change to delete.
* @return future for the deleting task.
*/
- public CheckedFuture<?, IOException> deleteAsync(Change.Id id) {
+ @SuppressWarnings("deprecation")
+ public com.google.common.util.concurrent.CheckedFuture<?, IOException> deleteAsync(Change.Id id) {
return submit(new DeleteTask(id));
}
@@ -300,7 +304,9 @@
* @param id ID of the change to index.
* @return future for reindexing the change; returns true if the change was stale.
*/
- public CheckedFuture<Boolean, IOException> reindexIfStale(Project.NameKey project, Change.Id id) {
+ @SuppressWarnings("deprecation")
+ public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
+ Project.NameKey project, Change.Id id) {
return submit(new ReindexIfStaleTask(project, id), batchExecutor);
}
@@ -324,11 +330,14 @@
return indexes != null ? indexes.getWriteIndexes() : Collections.singleton(index);
}
- private <T> CheckedFuture<T, IOException> submit(Callable<T> task) {
+ @SuppressWarnings("deprecation")
+ private <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
+ Callable<T> task) {
return submit(task, executor);
}
- private static <T> CheckedFuture<T, IOException> submit(
+ @SuppressWarnings("deprecation")
+ private static <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit(
Callable<T> task, ListeningExecutorService executor) {
return Futures.makeChecked(Futures.nonCancellationPropagating(executor.submit(task)), MAPPER);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
index a9f5306..2ab5c55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -90,11 +90,7 @@
Account.Id accountId = Account.Id.fromRef(event.getRefName());
if (accountId != null) {
try {
- if (event.isDelete()) {
- // TODO(ekempin): Delete account from cache and index.
- } else {
- accountCache.evict(accountId);
- }
+ accountCache.evict(accountId);
} catch (IOException e) {
log.error(String.format("Reindex account %s failed.", accountId), e);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 6b3492a..45cf244 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -22,7 +22,6 @@
import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
import com.google.auto.value.AutoValue;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
@@ -39,9 +38,9 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.InsertedObject;
-import com.google.gerrit.server.git.LockFailureException;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.update.ChainedReceiveCommands;
+import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -55,9 +54,7 @@
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
@@ -548,54 +545,11 @@
bru.setAllowNonFastForwards(true);
if (!dryrun) {
- bru.execute(or.rw, NullProgressMonitor.INSTANCE);
- checkResults(bru);
+ RefUpdateUtil.executeChecked(bru, or.rw);
}
return bru;
}
- /**
- * Check results of all commands in the update batch, reducing to a single exception if there was
- * a failure.
- *
- * <p>Throws {@link LockFailureException} if at least one command failed with {@code
- * LOCK_FAILURE}, and the entire transaction was aborted, i.e. any non-{@code LOCK_FAILURE}
- * results, if there were any, failed with "transaction aborted".
- *
- * <p>In particular, if the underlying ref database does not {@link
- * org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions() perform atomic transactions},
- * then a combination of {@code LOCK_FAILURE} on one ref and {@code OK} or another result on other
- * refs will <em>not</em> throw {@code LockFailureException}.
- *
- * @param bru batch update; should already have been executed.
- * @throws LockFailureException if the transaction was aborted due to lock failure.
- * @throws IOException if any result was not {@code OK}.
- */
- @VisibleForTesting
- static void checkResults(BatchRefUpdate bru) throws LockFailureException, IOException {
- int lockFailure = 0;
- int aborted = 0;
- int failure = 0;
-
- for (ReceiveCommand cmd : bru.getCommands()) {
- if (cmd.getResult() != ReceiveCommand.Result.OK) {
- failure++;
- }
- if (cmd.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
- lockFailure++;
- } else if (cmd.getResult() == ReceiveCommand.Result.REJECTED_OTHER_REASON
- && JGitText.get().transactionAborted.equals(cmd.getMessage())) {
- aborted++;
- }
- }
-
- if (lockFailure + aborted == bru.getCommands().size()) {
- throw new LockFailureException("Update aborted with one or more lock failures: " + bru);
- } else if (failure > 0) {
- throw new IOException("Update failed: " + bru);
- }
- }
-
private void addCommands() throws OrmException, IOException {
if (isEmpty()) {
return;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
index 41e8fbc..278b2af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
@@ -17,21 +17,24 @@
import com.google.common.collect.Lists;
import com.google.gerrit.common.errors.PermissionDeniedException;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.git.BanCommitResult;
+import com.google.gerrit.server.project.BanCommit.BanResultInfo;
import com.google.gerrit.server.project.BanCommit.Input;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.gerrit.server.update.UpdateException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
-import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.lib.ObjectId;
@Singleton
-public class BanCommit implements RestModifyView<ProjectResource, Input> {
+public class BanCommit extends RetryingRestModifyView<ProjectResource, Input, BanResultInfo> {
public static class Input {
public List<String> commits;
public String reason;
@@ -50,13 +53,15 @@
private final com.google.gerrit.server.git.BanCommit banCommit;
@Inject
- BanCommit(com.google.gerrit.server.git.BanCommit banCommit) {
+ BanCommit(RetryHelper retryHelper, com.google.gerrit.server.git.BanCommit banCommit) {
+ super(retryHelper);
this.banCommit = banCommit;
}
@Override
- public BanResultInfo apply(ProjectResource rsrc, Input input)
- throws UnprocessableEntityException, AuthException, ResourceConflictException, IOException {
+ protected BanResultInfo applyImpl(
+ BatchUpdate.Factory updateFactory, ProjectResource rsrc, Input input)
+ throws RestApiException, UpdateException, IOException {
BanResultInfo r = new BanResultInfo();
if (input != null && input.commits != null && !input.commits.isEmpty()) {
List<ObjectId> commitsToBan = new ArrayList<>(input.commits.size());
@@ -75,8 +80,6 @@
r.ignored = transformCommits(result.getIgnoredObjectIds());
} catch (PermissionDeniedException e) {
throw new AuthException(e.getMessage());
- } catch (ConcurrentRefUpdateException e) {
- throw new ResourceConflictException(e.getMessage(), e);
}
}
return r;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
index af55b00..fd0c7fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
@@ -17,7 +17,6 @@
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.CheckedFuture;
import com.google.common.util.concurrent.Futures;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
@@ -51,7 +50,9 @@
return new ListResultSet<>(ImmutableList.of());
}
- private static <T, K extends Key<?>> CheckedFuture<T, OrmException> emptyFuture() {
+ @SuppressWarnings("deprecation")
+ private static <T, K extends Key<?>>
+ com.google.common.util.concurrent.CheckedFuture<T, OrmException> emptyFuture() {
return Futures.immediateCheckedFuture(null);
}
@@ -164,8 +165,9 @@
return empty();
}
+ @SuppressWarnings("deprecation")
@Override
- public final CheckedFuture<T, OrmException> getAsync(K key) {
+ public final com.google.common.util.concurrent.CheckedFuture<T, OrmException> getAsync(K key) {
return emptyFuture();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
index f8ef5f9..b979636 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
@@ -21,7 +21,6 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.CheckedFuture;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change;
@@ -81,7 +80,9 @@
setRequestIds(updates, requestId);
try {
- List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>();
+ @SuppressWarnings("deprecation")
+ List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
+ new ArrayList<>();
List<ChangesHandle> handles = new ArrayList<>(updates.size());
Order order = getOrder(updates, listener);
try {
@@ -357,12 +358,14 @@
FusedNoteDbBatchUpdate.this.batchRefUpdate = manager.execute(dryrun);
}
- List<CheckedFuture<?, IOException>> startIndexFutures() {
+ @SuppressWarnings("deprecation")
+ List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> startIndexFutures() {
if (dryrun) {
return ImmutableList.of();
}
logDebug("Reindexing {} changes", results.size());
- List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>(results.size());
+ List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
+ new ArrayList<>(results.size());
for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) {
Change.Id id = e.getKey();
switch (e.getValue()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RefUpdateUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RefUpdateUtil.java
new file mode 100644
index 0000000..ab0b78e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RefUpdateUtil.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2017 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.update;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.server.git.LockFailureException;
+import java.io.IOException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Static utilities for working with JGit's ref update APIs. */
+public class RefUpdateUtil {
+ /**
+ * Execute a batch ref update, throwing a checked exception if not all updates succeeded.
+ *
+ * @param bru batch update; should already have been executed.
+ * @throws LockFailureException if the transaction was aborted due to lock failure; see {@link
+ * #checkResults(BatchRefUpdate)} for details.
+ * @throws IOException if any result was not {@code OK}.
+ */
+ public static void executeChecked(BatchRefUpdate bru, RevWalk rw) throws IOException {
+ bru.execute(rw, NullProgressMonitor.INSTANCE);
+ checkResults(bru);
+ }
+
+ /**
+ * Check results of all commands in the update batch, reducing to a single exception if there was
+ * a failure.
+ *
+ * <p>Throws {@link LockFailureException} if at least one command failed with {@code
+ * LOCK_FAILURE}, and the entire transaction was aborted, i.e. any non-{@code LOCK_FAILURE}
+ * results, if there were any, failed with "transaction aborted".
+ *
+ * <p>In particular, if the underlying ref database does not {@link
+ * org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions() perform atomic transactions},
+ * then a combination of {@code LOCK_FAILURE} on one ref and {@code OK} or another result on other
+ * refs will <em>not</em> throw {@code LockFailureException}.
+ *
+ * @param bru batch update; should already have been executed.
+ * @throws LockFailureException if the transaction was aborted due to lock failure.
+ * @throws IOException if any result was not {@code OK}.
+ */
+ @VisibleForTesting
+ static void checkResults(BatchRefUpdate bru) throws IOException {
+ int lockFailure = 0;
+ int aborted = 0;
+ int failure = 0;
+
+ for (ReceiveCommand cmd : bru.getCommands()) {
+ if (cmd.getResult() != ReceiveCommand.Result.OK) {
+ failure++;
+ }
+ if (cmd.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
+ lockFailure++;
+ } else if (cmd.getResult() == ReceiveCommand.Result.REJECTED_OTHER_REASON
+ && JGitText.get().transactionAborted.equals(cmd.getMessage())) {
+ aborted++;
+ }
+ }
+
+ if (lockFailure + aborted == bru.getCommands().size()) {
+ throw new LockFailureException("Update aborted with one or more lock failures: " + bru);
+ } else if (failure > 0) {
+ throw new IOException("Update failed: " + bru);
+ }
+ }
+
+ private RefUpdateUtil() {}
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index 3cc2f9e..1561fc4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -23,7 +23,6 @@
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.CheckedFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
@@ -321,7 +320,10 @@
private final ReviewDb db;
private final SchemaFactory<ReviewDb> schemaFactory;
private final long skewMs;
- private final List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>();
+
+ @SuppressWarnings("deprecation")
+ private final List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures =
+ new ArrayList<>();
@Inject
ReviewDbBatchUpdate(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
index ab4b701..b5c7256 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
@@ -22,7 +22,6 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
-import com.google.common.util.concurrent.CheckedFuture;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change;
@@ -250,7 +249,8 @@
private final GitReferenceUpdated gitRefUpdated;
private final ReviewDb db;
- private List<CheckedFuture<?, IOException>> indexFutures;
+ @SuppressWarnings("deprecation")
+ private List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures;
@Inject
UnfusedNoteDbBatchUpdate(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/CommitMessageUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/CommitMessageUtil.java
new file mode 100644
index 0000000..fa55597
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/CommitMessageUtil.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2017 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.util;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+
+/** Utility functions to manipulate commit messages. */
+public class CommitMessageUtil {
+
+ private CommitMessageUtil() {}
+
+ /**
+ * Checks for null or empty commit messages and appends a newline character to the commit message.
+ *
+ * @throws BadRequestException if the commit message is null or empty
+ * @returns the trimmed message with a trailing newline character
+ */
+ public static String checkAndSanitizeCommitMessage(String commitMessage)
+ throws BadRequestException {
+ String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim();
+ if (wellFormedMessage.isEmpty()) {
+ throw new BadRequestException("Commit message cannot be null or empty");
+ }
+ wellFormedMessage = wellFormedMessage + "\n";
+ return wellFormedMessage;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java
index dc7dd3d..8e8db12 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java
@@ -47,7 +47,7 @@
private final String str;
private RequestId(String resourceId) {
- Hasher h = Hashing.sha1().newHasher();
+ Hasher h = Hashing.murmur3_128().newHasher();
h.putLong(Thread.currentThread().getId()).putUnencodedChars(MACHINE_ID);
str =
"["
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbUpdateManagerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/update/RefUpdateUtilTest.java
similarity index 91%
rename from gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbUpdateManagerTest.java
rename to gerrit-server/src/test/java/com/google/gerrit/server/update/RefUpdateUtilTest.java
index 12eb39d..286827a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbUpdateManagerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/update/RefUpdateUtilTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.notedb;
+package com.google.gerrit.server.update;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertThat;
@@ -32,9 +32,8 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-/** Unit tests for {@link NoteDbUpdateManager}. */
@RunWith(JUnit4.class)
-public class NoteDbUpdateManagerTest {
+public class RefUpdateUtilTest {
private static final Consumer<ReceiveCommand> OK = c -> c.setResult(ReceiveCommand.Result.OK);
private static final Consumer<ReceiveCommand> LOCK_FAILURE =
c -> c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
@@ -76,13 +75,13 @@
@SafeVarargs
private static void checkResults(Consumer<ReceiveCommand>... resultSetters) throws Exception {
- NoteDbUpdateManager.checkResults(newBatchRefUpdate(resultSetters));
+ RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
}
@SafeVarargs
private static void assertIoException(Consumer<ReceiveCommand>... resultSetters) {
try {
- NoteDbUpdateManager.checkResults(newBatchRefUpdate(resultSetters));
+ RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
assert_().fail("expected IOException");
} catch (IOException e) {
assertThat(e).isNotInstanceOf(LockFailureException.class);
@@ -93,7 +92,7 @@
private static void assertLockFailureException(Consumer<ReceiveCommand>... resultSetters)
throws Exception {
try {
- NoteDbUpdateManager.checkResults(newBatchRefUpdate(resultSetters));
+ RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters));
assert_().fail("expected LockFailureException");
} catch (LockFailureException e) {
// Expected.
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 78726a0..22d7e9a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -18,7 +18,6 @@
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.project.BanCommit;
import com.google.gerrit.server.project.BanCommit.BanResultInfo;
import com.google.gerrit.server.project.ProjectControl;
@@ -26,7 +25,6 @@
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.lib.ObjectId;
@@ -77,7 +75,7 @@
printCommits(r.newlyBanned, "The following commits were banned");
printCommits(r.alreadyBanned, "The following commits were already banned");
printCommits(r.ignored, "The following ids do not represent commits and were ignored");
- } catch (RestApiException | IOException e) {
+ } catch (Exception e) {
throw die(e);
}
}
diff --git a/lib/guava.bzl b/lib/guava.bzl
index c71379e..768b99e 100644
--- a/lib/guava.bzl
+++ b/lib/guava.bzl
@@ -1,5 +1,5 @@
-GUAVA_VERSION = "21.0"
+GUAVA_VERSION = "22.0"
-GUAVA_BIN_SHA1 = "3a3d111be1be1b745edfa7d91678a12d7ed38709"
+GUAVA_BIN_SHA1 = "3564ef3803de51fb0530a8377ec6100b33b0d073"
GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 1a64a6f..f0930e9 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 1a64a6f6407df31c06350a5c6e9266e9ba0cf72a
+Subproject commit f0930e9dfc56644105cb21e7246096097eeaa385