Merge "Adds diff traversal helper and make diff object a builder property"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index abe0606..ff6e94a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2846,9 +2846,7 @@
Each element of the `reviewers` list is an instance of
link:#reviewer-input[ReviewerInput]. The corresponding result of
adding each reviewer will be returned in a list of
-link:#add-reviewer-result[AddReviewerResult]. If there are any
-errors returned for reviewers, the entire review request will
-be rejected.
+link:#add-reviewer-result[AddReviewerResult].
.Response
----
@@ -2886,6 +2884,27 @@
}
----
+If there are any errors returned for reviewers, the entire review request will
+be rejected with `400 Bad Request`.
+
+.Error Response
+----
+ HTTP/1.1 400 Bad Request
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "reviewers": {
+ "MyProjectVerifiers": {
+ "input": "MyProjectVerifiers",
+ "error": "The group My Group has 15 members. Do you want to add them all as reviewers?",
+ "confirm": true
+ }
+ }
+ }
+----
+
[[rebase-revision]]
=== Rebase Revision
--
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 b32fccc..324aad2 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
@@ -42,6 +42,7 @@
import com.google.gerrit.extensions.api.projects.ProjectInput;
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.ActionInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.EditInfo;
@@ -415,15 +416,28 @@
protected Project.NameKey createProject(String nameSuffix,
Project.NameKey parent) throws RestApiException {
// Default for createEmptyCommit should match TestProjectConfig.
- return createProject(nameSuffix, parent, true);
+ return createProject(nameSuffix, parent, true, null);
}
protected Project.NameKey createProject(String nameSuffix,
- Project.NameKey parent, boolean createEmptyCommit)
+ Project.NameKey parent, boolean createEmptyCommit) throws RestApiException {
+ // Default for createEmptyCommit should match TestProjectConfig.
+ return createProject(nameSuffix, parent, createEmptyCommit, null);
+ }
+
+ protected Project.NameKey createProject(String nameSuffix,
+ Project.NameKey parent, SubmitType submitType) throws RestApiException {
+ // Default for createEmptyCommit should match TestProjectConfig.
+ return createProject(nameSuffix, parent, true, submitType);
+ }
+
+ protected Project.NameKey createProject(String nameSuffix,
+ Project.NameKey parent, boolean createEmptyCommit, SubmitType submitType)
throws RestApiException {
ProjectInput in = new ProjectInput();
in.name = name(nameSuffix);
in.parent = parent != null ? parent.get() : null;
+ in.submitType = submitType;
in.createEmptyCommit = createEmptyCommit;
return createProject(in);
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index af13c5d..2ca4691 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -21,6 +21,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
@@ -40,14 +41,52 @@
import java.util.concurrent.atomic.AtomicInteger;
public abstract class AbstractSubmoduleSubscription extends AbstractDaemonTest {
+
+ protected SubmitType getSubmitType() {
+ return cfg.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
+ }
+
+ protected static Config submitByMergeAlways() {
+ Config cfg = new Config();
+ cfg.setBoolean("change", null, "submitWholeTopic", true);
+ cfg.setEnum("project", null, "submitType", SubmitType.MERGE_ALWAYS);
+ return cfg;
+ }
+
+ protected static Config submitByMergeIfNecessary() {
+ Config cfg = new Config();
+ cfg.setBoolean("change", null, "submitWholeTopic", true);
+ cfg.setEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
+ return cfg;
+ }
+
+ protected static Config submitByCherryPickConifg() {
+ Config cfg = new Config();
+ cfg.setBoolean("change", null, "submitWholeTopic", true);
+ cfg.setEnum("project", null, "submitType", SubmitType.CHERRY_PICK);
+ return cfg;
+ }
+
+ protected static Config submitByRebaseConifg() {
+ Config cfg = new Config();
+ cfg.setBoolean("change", null, "submitWholeTopic", true);
+ cfg.setEnum("project", null, "submitType", SubmitType.REBASE_IF_NECESSARY);
+ return cfg;
+ }
+
protected TestRepository<?> createProjectWithPush(String name,
- @Nullable Project.NameKey parent) throws Exception {
- Project.NameKey project = createProject(name, parent);
+ @Nullable Project.NameKey parent, SubmitType submitType) throws Exception {
+ Project.NameKey project = createProject(name, parent, submitType);
grant(Permission.PUSH, project, "refs/heads/*");
grant(Permission.SUBMIT, project, "refs/for/refs/heads/*");
return cloneProject(project);
}
+ protected TestRepository<?> createProjectWithPush(String name,
+ @Nullable Project.NameKey parent) throws Exception {
+ return createProjectWithPush(name, parent, getSubmitType());
+ }
+
protected TestRepository<?> createProjectWithPush(String name)
throws Exception {
return createProjectWithPush(name, null);
@@ -56,10 +95,11 @@
private static AtomicInteger contentCounter = new AtomicInteger(0);
protected ObjectId pushChangeTo(TestRepository<?> repo, String ref,
- String message, String topic) throws Exception {
+ String file, String content, String message, String topic)
+ throws Exception {
ObjectId ret = repo.branch("HEAD").commit().insertChangeId()
.message(message)
- .add("a.txt", "a contents: " + contentCounter.incrementAndGet())
+ .add(file, content)
.create();
String pushedRef = ref;
@@ -79,6 +119,12 @@
return ret;
}
+ protected ObjectId pushChangeTo(TestRepository<?> repo, String ref,
+ String message, String topic) throws Exception {
+ return pushChangeTo(repo, ref, "a.txt",
+ "a contents: " + contentCounter.incrementAndGet(), message, topic);
+ }
+
protected ObjectId pushChangeTo(TestRepository<?> repo, String branch)
throws Exception {
return pushChangeTo(repo, "refs/heads/" + branch, "some change", "");
@@ -135,16 +181,25 @@
protected void prepareSubmoduleConfigEntry(Config config,
String subscribeToRepo, String subscribeToBranch) {
+ // The submodule subscription module checks for gerrit.canonicalWebUrl to
+ // detect if it's configured for automatic updates. It doesn't matter if
+ // it serves from that URL.
+ prepareSubmoduleConfigEntry(config, subscribeToRepo, subscribeToRepo, subscribeToBranch);
+ }
+
+ protected void prepareSubmoduleConfigEntry(Config config,
+ String subscribeToRepo, String subscribeToRepoPath, String subscribeToBranch) {
subscribeToRepo = name(subscribeToRepo);
+ subscribeToRepoPath = name(subscribeToRepoPath);
// The submodule subscription module checks for gerrit.canonicalWebUrl to
// detect if it's configured for automatic updates. It doesn't matter if
// it serves from that URL.
String url = cfg.getString("gerrit", null, "canonicalWebUrl") + "/"
+ subscribeToRepo;
- config.setString("submodule", subscribeToRepo, "path", subscribeToRepo);
- config.setString("submodule", subscribeToRepo, "url", url);
+ config.setString("submodule", subscribeToRepoPath, "path", subscribeToRepoPath);
+ config.setString("submodule", subscribeToRepoPath, "url", url);
if (subscribeToBranch != null) {
- config.setString("submodule", subscribeToRepo, "branch", subscribeToBranch);
+ config.setString("submodule", subscribeToRepoPath, "branch", subscribeToBranch);
}
}
@@ -161,6 +216,27 @@
}
protected void expectToHaveSubmoduleState(TestRepository<?> repo,
+ String branch, String submodule, TestRepository<?> subRepo,
+ String subBranch) throws Exception {
+
+ submodule = name(submodule);
+ ObjectId commitId = repo.git().fetch().setRemote("origin").call()
+ .getAdvertisedRef("refs/heads/" + branch).getObjectId();
+
+ ObjectId subHead = subRepo.git().fetch().setRemote("origin").call()
+ .getAdvertisedRef("refs/heads/" + subBranch).getObjectId();
+
+ RevWalk rw = repo.getRevWalk();
+ RevCommit c = rw.parseCommit(commitId);
+ rw.parseBody(c.getTree());
+
+ RevTree tree = c.getTree();
+ RevObject actualId = repo.get(tree, submodule);
+
+ assertThat(actualId).isEqualTo(subHead);
+ }
+
+ protected void expectToHaveSubmoduleState(TestRepository<?> repo,
String branch, String submodule, ObjectId expectedId) throws Exception {
submodule = name(submodule);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 8156f67..8423bbf 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -215,21 +215,18 @@
// The first update doesn't include the rev log
RevWalk rw = subRepo.getRevWalk();
- RevCommit subCommitMsg = rw.parseCommit(subHEAD);
expectToHaveCommitMessage(superRepo, "master",
"Update git submodules\n\n" +
- "Project: " + name("subscribed-to-project")
- + " master " + subHEAD.name() + "\n\n");
+ "* Update " + name("subscribed-to-project") + " from branch 'master'");
// The next commit should generate only its commit message,
// omitting previous commit logs
subHEAD = pushChangeTo(subRepo, "master");
- subCommitMsg = rw.parseCommit(subHEAD);
+ RevCommit subCommitMsg = rw.parseCommit(subHEAD);
expectToHaveCommitMessage(superRepo, "master",
"Update git submodules\n\n" +
- "Project: " + name("subscribed-to-project")
- + " master " + subHEAD.name() + "\n\n" +
- subCommitMsg.getFullMessage() + "\n\n");
+ "* Update " + name("subscribed-to-project") + " from branch 'master'"
+ + "\n - " + subCommitMsg.getFullMessage().replace("\n", "\n "));
}
@Test
@@ -302,7 +299,7 @@
}
@Test
- public void testCircularSubscriptionIsDetected() throws Exception {
+ public void testBranchCircularSubscription() throws Exception {
TestRepository<?> superRepo = createProjectWithPush("super-project");
TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
@@ -325,6 +322,37 @@
"subscribed-to-project")).isFalse();
}
+ @Test
+ public void testProjectCircularSubscription() throws Exception {
+ TestRepository<?> superRepo = createProjectWithPush("super-project");
+ TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+ allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+ "super-project", "refs/heads/master");
+ allowSubmoduleSubscription("super-project", "refs/heads/dev",
+ "subscribed-to-project", "refs/heads/dev");
+
+ pushChangeTo(subRepo, "master");
+ pushChangeTo(superRepo, "master");
+ pushChangeTo(subRepo, "dev");
+ pushChangeTo(superRepo, "dev");
+
+ createSubmoduleSubscription(superRepo, "master",
+ "subscribed-to-project", "master");
+ createSubmoduleSubscription(subRepo, "dev", "super-project", "dev");
+
+ ObjectId subMasterHead = pushChangeTo(subRepo, "master");
+ ObjectId superDevHead = pushChangeTo(superRepo, "dev");
+
+ assertThat(hasSubmodule(superRepo, "master",
+ "subscribed-to-project")).isTrue();
+ assertThat(hasSubmodule(subRepo, "dev",
+ "super-project")).isTrue();
+ expectToHaveSubmoduleState(superRepo, "master",
+ "subscribed-to-project", subMasterHead);
+ expectToHaveSubmoduleState(subRepo, "dev",
+ "super-project", superDevHead);
+ }
@Test
public void testSubscriptionFailOnMissingACL() throws Exception {
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 0a067e9..dfce62e 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
@@ -14,11 +14,13 @@
package com.google.gerrit.acceptance.git;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.acceptance.GitUtil.getChangeId;
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.testutil.ConfigSuite;
import org.eclipse.jgit.junit.TestRepository;
@@ -33,8 +35,23 @@
extends AbstractSubmoduleSubscription {
@ConfigSuite.Default
- public static Config submitWholeTopicEnabled() {
- return submitWholeTopicEnabledConfig();
+ public static Config mergeIfNecessary() {
+ return submitByMergeIfNecessary();
+ }
+
+ @ConfigSuite.Config
+ public static Config mergeAlways() {
+ return submitByMergeAlways();
+ }
+
+ @ConfigSuite.Config
+ public static Config cherryPick() {
+ return submitByCherryPickConifg();
+ }
+
+ @ConfigSuite.Config
+ public static Config rebase() {
+ return submitByRebaseConifg();
}
@Test
@@ -192,9 +209,9 @@
gApi.changes().id(getChangeId(sub1, sub1Id).get()).current().submit();
- expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1Id);
- expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2Id);
- expectToHaveSubmoduleState(superRepo, "master", "sub3", sub3Id);
+ expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1, "master");
+ expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
+ expectToHaveSubmoduleState(superRepo, "master", "sub3", sub3, "master");
superRepo.git().fetch().setRemote("origin").call()
.getAdvertisedRef("refs/heads/master").getObjectId();
@@ -204,4 +221,216 @@
.that(superRepo.getRepository().resolve("origin/master^"))
.isEqualTo(superPreviousId);
}
+
+ @Test
+ public void testDifferentPaths() throws Exception {
+ TestRepository<?> superRepo = createProjectWithPush("super-project");
+ TestRepository<?> sub = createProjectWithPush("sub");
+
+ allowSubmoduleSubscription("sub", "refs/heads/master",
+ "super-project", "refs/heads/master");
+
+ Config config = new Config();
+ prepareSubmoduleConfigEntry(config, "sub", "master");
+ prepareSubmoduleConfigEntry(config, "sub", "sub-copy", "master");
+ pushSubmoduleConfig(superRepo, "master", config);
+
+ ObjectId superPreviousId = pushChangeTo(superRepo, "master");
+
+ ObjectId subId = pushChangeTo(sub, "refs/for/master", "some message", "");
+
+ approve(getChangeId(sub, subId).get());
+
+ gApi.changes().id(getChangeId(sub, subId).get()).current().submit();
+
+ expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
+ expectToHaveSubmoduleState(superRepo, "master", "sub-copy", sub, "master");
+
+ superRepo.git().fetch().setRemote("origin").call()
+ .getAdvertisedRef("refs/heads/master").getObjectId();
+
+ assertWithMessage("submodule subscription update "
+ + "should have made one commit")
+ .that(superRepo.getRepository().resolve("origin/master^"))
+ .isEqualTo(superPreviousId);
+ }
+
+ @Test
+ public void testNonSubmoduleInSameTopic() throws Exception {
+ TestRepository<?> superRepo = createProjectWithPush("super-project");
+ TestRepository<?> sub = createProjectWithPush("sub");
+ TestRepository<?> standAlone = createProjectWithPush("standalone");
+
+ allowSubmoduleSubscription("sub", "refs/heads/master",
+ "super-project", "refs/heads/master");
+
+ createSubmoduleSubscription(superRepo, "master", "sub", "master");
+
+ ObjectId superPreviousId = pushChangeTo(superRepo, "master");
+
+ ObjectId subId =
+ pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+ ObjectId standAloneId =
+ pushChangeTo(standAlone, "refs/for/master", "some message",
+ "same-topic");
+
+ String subChangeId = getChangeId(sub, subId).get();
+ String standAloneChangeId = getChangeId(standAlone, standAloneId).get();
+ approve(subChangeId);
+ approve(standAloneChangeId);
+
+ gApi.changes().id(subChangeId).current().submit();
+
+ expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
+
+ ChangeStatus status = gApi.changes().id(standAloneChangeId).info().status;
+ assertThat(status).isEqualTo(ChangeStatus.MERGED);
+
+ superRepo.git().fetch().setRemote("origin").call()
+ .getAdvertisedRef("refs/heads/master").getObjectId();
+
+ assertWithMessage("submodule subscription update "
+ + "should have made one commit")
+ .that(superRepo.getRepository().resolve("origin/master^"))
+ .isEqualTo(superPreviousId);
+ }
+
+ @Test
+ public void testRecursiveSubmodules() throws Exception {
+ TestRepository<?> topRepo = createProjectWithPush("top-project");
+ TestRepository<?> midRepo = createProjectWithPush("mid-project");
+ TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+
+ allowSubmoduleSubscription("mid-project", "refs/heads/master",
+ "top-project", "refs/heads/master");
+ allowSubmoduleSubscription("bottom-project", "refs/heads/master",
+ "mid-project", "refs/heads/master");
+
+ createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
+ createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+
+ ObjectId bottomHead =
+ pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
+ ObjectId topHead =
+ pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
+
+ String id1 = getChangeId(bottomRepo, bottomHead).get();
+ String id2 = getChangeId(topRepo, topHead).get();
+
+ gApi.changes().id(id1).current().review(ReviewInput.approve());
+ gApi.changes().id(id2).current().review(ReviewInput.approve());
+
+ gApi.changes().id(id1).current().submit();
+
+ expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
+ expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
+ }
+
+ @Test
+ public void testTriangleSubmodules() throws Exception {
+ TestRepository<?> topRepo = createProjectWithPush("top-project");
+ TestRepository<?> midRepo = createProjectWithPush("mid-project");
+ TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+
+ allowSubmoduleSubscription("mid-project", "refs/heads/master",
+ "top-project", "refs/heads/master");
+ allowSubmoduleSubscription("bottom-project", "refs/heads/master",
+ "mid-project", "refs/heads/master");
+ allowSubmoduleSubscription("bottom-project", "refs/heads/master",
+ "top-project", "refs/heads/master");
+
+ createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+ Config config = new Config();
+ prepareSubmoduleConfigEntry(config, "bottom-project", "master");
+ prepareSubmoduleConfigEntry(config, "mid-project", "master");
+ pushSubmoduleConfig(topRepo, "master", config);
+
+ ObjectId bottomHead =
+ pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
+ ObjectId topHead =
+ pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
+
+ String id1 = getChangeId(bottomRepo, bottomHead).get();
+ String id2 = getChangeId(topRepo, topHead).get();
+
+ gApi.changes().id(id1).current().review(ReviewInput.approve());
+ gApi.changes().id(id2).current().review(ReviewInput.approve());
+
+ gApi.changes().id(id1).current().submit();
+
+ expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
+ expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
+ expectToHaveSubmoduleState(topRepo, "master", "bottom-project", bottomRepo, "master");
+ }
+
+ @Test
+ public void testBranchCircularSubscription() throws Exception {
+ TestRepository<?> topRepo = createProjectWithPush("top-project");
+ TestRepository<?> midRepo = createProjectWithPush("mid-project");
+ TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
+
+ createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
+ createSubmoduleSubscription(topRepo, "master", "mid-project", "master");
+ createSubmoduleSubscription(bottomRepo, "master", "top-project", "master");
+
+ allowSubmoduleSubscription("bottom-project", "refs/heads/master",
+ "mid-project", "refs/heads/master");
+ allowSubmoduleSubscription("mid-project", "refs/heads/master",
+ "top-project", "refs/heads/master");
+ allowSubmoduleSubscription("top-project", "refs/heads/master",
+ "bottom-project", "refs/heads/master");
+
+ ObjectId bottomMasterHead =
+ pushChangeTo(bottomRepo, "refs/for/master", "some message", "");
+ String changeId = getChangeId(bottomRepo, bottomMasterHead).get();
+
+ approve(changeId);
+
+ exception.expectMessage("Branch level circular subscriptions detected");
+ exception.expectMessage("top-project,refs/heads/master");
+ exception.expectMessage("mid-project,refs/heads/master");
+ exception.expectMessage("bottom-project,refs/heads/master");
+ gApi.changes().id(changeId).current().submit();
+
+ assertThat(hasSubmodule(midRepo, "master", "bottom-project")).isFalse();
+ assertThat(hasSubmodule(topRepo, "master", "mid-project")).isFalse();
+ }
+
+ @Test
+ public void testProjectCircularSubscriptionWholeTopic() throws Exception {
+ TestRepository<?> superRepo = createProjectWithPush("super-project");
+ TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+ allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+ "super-project", "refs/heads/master");
+ allowSubmoduleSubscription("super-project", "refs/heads/dev",
+ "subscribed-to-project", "refs/heads/dev");
+
+ pushChangeTo(subRepo, "dev");
+ pushChangeTo(superRepo, "dev");
+
+ createSubmoduleSubscription(superRepo, "master",
+ "subscribed-to-project", "master");
+ createSubmoduleSubscription(subRepo, "dev", "super-project", "dev");
+
+ ObjectId subMasterHead =
+ pushChangeTo(subRepo, "refs/for/master", "b.txt", "content b",
+ "some message", "same-topic");
+ ObjectId superDevHead =
+ pushChangeTo(superRepo, "refs/for/dev",
+ "some message", "same-topic");
+
+ approve(getChangeId(subRepo, subMasterHead).get());
+ approve(getChangeId(superRepo, superDevHead).get());
+
+ exception.expectMessage("Project level circular subscriptions detected");
+ exception.expectMessage("subscribed-to-project");
+ exception.expectMessage("super-project");
+ gApi.changes().id(getChangeId(subRepo, subMasterHead).get()).current()
+ .submit();
+
+ assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project"))
+ .isFalse();
+ assertThat(hasSubmodule(subRepo, "dev", "super-project")).isFalse();
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 49fb102..2b32c9f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -17,6 +17,8 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.extensions.client.ReviewerState.CC;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -272,7 +274,7 @@
// In legacy mode, change owner should be the only reviewer.
assertReviewers(c, REVIEWER, admin);
assertReviewers(c, CC, user, observer);
- }
+ }
// Verify emails were sent to added reviewers.
List<Message> messages = sender.getMessages();
@@ -329,7 +331,8 @@
.reviewer(user.email)
.reviewer(observer.email, CC, false)
.reviewer(largeGroup);
- ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+ ReviewResult result = review(
+ r.getChangeId(), r.getCommit().name(), input, SC_BAD_REQUEST);
assertThat(result.labels).isNull();
assertThat(result.reviewers).isNotNull();
assertThat(result.reviewers).hasSize(3);
@@ -353,7 +356,8 @@
.reviewer(user.email)
.reviewer(observer.email, CC, false)
.reviewer(mediumGroup);
- result = review(r.getChangeId(), r.getCommit().name(), input);
+ result = review(r.getChangeId(), r.getCommit().name(), input,
+ SC_BAD_REQUEST);
assertThat(result.labels).isNull();
assertThat(result.reviewers).isNotNull();
assertThat(result.reviewers).hasSize(3);
@@ -400,27 +404,46 @@
private AddReviewerResult addReviewer(String changeId, String reviewer)
throws Exception {
+ return addReviewer(changeId, reviewer, SC_OK);
+ }
+
+ private AddReviewerResult addReviewer(
+ String changeId, String reviewer, int expectedStatus) throws Exception {
AddReviewerInput in = new AddReviewerInput();
in.reviewer = reviewer;
- return addReviewer(changeId, in);
+ return addReviewer(changeId, in, expectedStatus);
}
private AddReviewerResult addReviewer(String changeId, AddReviewerInput in)
throws Exception {
+ return addReviewer(changeId, in, SC_OK);
+ }
+
+ private AddReviewerResult addReviewer(String changeId, AddReviewerInput in,
+ int expectedStatus) throws Exception {
RestResponse resp =
adminRestSession.post("/changes/" + changeId + "/reviewers", in);
- return readContentFromJson(resp, AddReviewerResult.class);
+ return readContentFromJson(
+ resp, expectedStatus, AddReviewerResult.class);
}
- private ReviewResult review(String changeId, String revisionId, ReviewInput in) throws Exception {
+ private ReviewResult review(
+ String changeId, String revisionId, ReviewInput in) throws Exception {
+ return review(changeId, revisionId, in, SC_OK);
+ }
+
+ private ReviewResult review(
+ String changeId, String revisionId, ReviewInput in, int expectedStatus)
+ throws Exception {
RestResponse resp = adminRestSession.post(
"/changes/" + changeId + "/revisions/" + revisionId + "/review", in);
- return readContentFromJson(resp, ReviewResult.class);
+ return readContentFromJson(resp, expectedStatus, ReviewResult.class);
}
- private static <T> T readContentFromJson(RestResponse r, Class<T> clazz)
+ private static <T> T readContentFromJson(
+ RestResponse r, int expectedStatus, Class<T> clazz)
throws Exception {
- r.assertOK();
+ r.assertStatus(expectedStatus);
JsonReader jsonReader = new JsonReader(r.getReader());
jsonReader.setLenient(true);
return newGson().fromJson(jsonReader, clazz);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
index 803a94a..633efea 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
@@ -52,6 +52,11 @@
return new Redirect(location);
}
+ /** Arbitrary status code with wrapped result. */
+ public static <T> Response<T> withStatusCode(int statusCode, T value) {
+ return new Impl<>(statusCode, value);
+ }
+
@SuppressWarnings({"unchecked", "rawtypes"})
public static <T> T unwrap(T obj) {
while (obj instanceof Response) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 5942f40..05a7179 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -150,7 +150,7 @@
*/
public <T> void put(PropertyKey<T> key, @Nullable T value) {
Cache<PropertyKey<Object>, Object> p = properties(value != null);
- if (p != null || value != null) {
+ if (p != null) {
@SuppressWarnings("unchecked")
PropertyKey<Object> k = (PropertyKey<Object>) key;
if (value != null) {
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 4e8527a..aa35da8 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
@@ -20,6 +20,7 @@
import static com.google.gerrit.server.change.PutDraftComment.side;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
@@ -48,6 +49,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
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.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -145,12 +147,12 @@
}
@Override
- public ReviewResult apply(RevisionResource revision, ReviewInput input)
+ public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input)
throws RestApiException, UpdateException, OrmException, IOException {
return apply(revision, input, TimeUtil.nowTs());
}
- public ReviewResult apply(RevisionResource revision, ReviewInput input,
+ public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input,
Timestamp ts)
throws RestApiException, UpdateException, OrmException, IOException {
// Respect timestamp, but truncate at change created-on time.
@@ -197,7 +199,7 @@
ReviewResult output = new ReviewResult();
output.reviewers = reviewerJsonResults;
if (hasError || confirm) {
- return output;
+ return Response.withStatusCode(SC_BAD_REQUEST, output);
}
output.labels = input.labels;
@@ -218,7 +220,7 @@
}
}
- return output;
+ return Response.ok(output);
}
private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
index e0c72c6..a5cb18d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
@@ -64,12 +64,12 @@
this.submissionId = orm.getSubmissionId();
Project.NameKey project = branch.getParentKey();
logDebug("Loading .gitmodules of {} for project {}", branch, project);
+ OpenRepo or;
try {
- orm.openRepo(project, false);
+ or = orm.openRepo(project, false);
} catch (NoSuchProjectException e) {
throw new IOException(e);
}
- OpenRepo or = orm.getRepo(project);
ObjectId id = or.repo.resolve(branch.get());
if (id == null) {
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 c69abf5..9a38e2d 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
@@ -479,7 +479,7 @@
throw new IntegrationException("Error reading changes to submit", e);
}
Set<Project.NameKey> projects = br.keySet();
- Collection<Branch.NameKey> branches = cbb.keySet();
+ Set<Branch.NameKey> branches = cbb.keySet();
openRepos(projects);
for (Branch.NameKey branch : branches) {
@@ -488,28 +488,18 @@
}
// Done checks that don't involve running submit strategies.
commits.maybeFailVerbose();
-
- List<SubmitStrategy> strategies = new ArrayList<>(branches.size());
- for (Branch.NameKey branch : branches) {
- OpenRepo or = orm.getRepo(branch.getParentKey());
- OpenBranch ob = or.getBranch(branch);
- BranchBatch submitting = toSubmit.get(branch);
- checkNotNull(submitting.submitType(),
- "null submit type for %s; expected to previously fail fast",
- submitting);
- Set<CodeReviewCommit> commitsToSubmit = commits(submitting.changes());
- ob.mergeTip = new MergeTip(ob.oldTip, commitsToSubmit);
- SubmitStrategy strategy = createStrategy(or, ob.mergeTip, branch,
- submitting.submitType(), ob.oldTip);
- strategies.add(strategy);
- strategy.addOps(or.getUpdate(), commitsToSubmit);
- }
-
+ SubmoduleOp submoduleOp = subOpFactory.create(branches, orm);
try {
+ List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, submoduleOp);
+ Set<Project.NameKey> allProjects = submoduleOp.getProjectsInOrder();
+ // in case superproject subscription is disabled, allProjects would be null
+ if (allProjects == null) {
+ allProjects = projects;
+ }
BatchUpdate.execute(
- batchUpdates(projects),
+ orm.batchUpdates(allProjects),
new SubmitStrategyListener(submitInput, strategies, commits));
- } catch (UpdateException e) {
+ } catch (UpdateException | SubmoduleException 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
@@ -521,20 +511,44 @@
if (e.getCause() instanceof IntegrationException) {
msg = e.getCause().getMessage();
} else {
- msg = "Error submitting change" + (cs.size() != 1 ? "s" : "");
+ msg = "Error submitting change" + (cs.size() != 1 ? "s" : "") + ": \n"
+ + e.getMessage();
}
throw new IntegrationException(msg, e);
}
-
- updateSuperProjects(br.values());
}
- private List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects) {
- List<BatchUpdate> updates = new ArrayList<>(projects.size());
- for (Project.NameKey project : projects) {
- updates.add(orm.getRepo(project).getUpdate());
+ private List<SubmitStrategy> getSubmitStrategies(
+ Map<Branch.NameKey, BranchBatch> toSubmit, SubmoduleOp submoduleOp)
+ throws IntegrationException {
+ List<SubmitStrategy> strategies = new ArrayList<>();
+ Set<Branch.NameKey> allBranches = submoduleOp.getBranchesInOrder();
+ // in case superproject subscription is disabled, allBranches would be null
+ if (allBranches == null) {
+ allBranches = toSubmit.keySet();
}
- return updates;
+
+ for (Branch.NameKey branch : allBranches) {
+ OpenRepo or = orm.getRepo(branch.getParentKey());
+ if (toSubmit.containsKey(branch)) {
+ BranchBatch submitting = toSubmit.get(branch);
+ OpenBranch ob = or.getBranch(branch);
+ checkNotNull(submitting.submitType(),
+ "null submit type for %s; expected to previously fail fast",
+ submitting);
+ Set<CodeReviewCommit> commitsToSubmit = commits(submitting.changes());
+ ob.mergeTip = new MergeTip(ob.oldTip, commitsToSubmit);
+ SubmitStrategy strategy = createStrategy(or, ob.mergeTip, branch,
+ submitting.submitType(), ob.oldTip, submoduleOp);
+ strategies.add(strategy);
+ strategy.addOps(or.getUpdate(), commitsToSubmit);
+ } else {
+ // no open change for this branch
+ // add submodule triggered op into BatchUpdate
+ submoduleOp.addOp(or.getUpdate(), branch);
+ }
+ }
+ return strategies;
}
private Set<CodeReviewCommit> commits(List<ChangeData> cds) {
@@ -551,10 +565,10 @@
private SubmitStrategy createStrategy(OpenRepo or,
MergeTip mergeTip, Branch.NameKey destBranch, SubmitType submitType,
- CodeReviewCommit branchTip) throws IntegrationException {
+ CodeReviewCommit branchTip, SubmoduleOp submoduleOp) throws IntegrationException {
return submitStrategyFactory.create(submitType, db, or.repo, or.rw, or.ins,
or.canMergeFlag, getAlreadyAccepted(or, branchTip), destBranch, caller,
- mergeTip, commits, submissionId, submitInput.notify);
+ mergeTip, commits, submissionId, submitInput.notify, submoduleOp);
}
private Set<RevCommit> getAlreadyAccepted(OpenRepo or,
@@ -729,18 +743,6 @@
}
}
- private void updateSuperProjects(Collection<Branch.NameKey> branches) {
- logDebug("Updating superprojects");
- SubmoduleOp subOp = subOpFactory.create(branches, orm);
- try {
- subOp.updateSuperProjects();
- logDebug("Updating superprojects done");
- } catch (SubmoduleException e) {
- logError("The gitlinks were not updated according to the "
- + "subscriptions", e);
- }
- }
-
private void openRepos(Collection<Project.NameKey> projects)
throws IntegrationException {
for (Project.NameKey project : projects) {
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 2bfe5b7..33e4c66 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
@@ -185,16 +185,17 @@
return or;
}
- public void openRepo(Project.NameKey project, boolean abortIfOpen)
+ public OpenRepo openRepo(Project.NameKey project, boolean abortIfOpen)
throws NoSuchProjectException, IOException {
if (abortIfOpen) {
checkState(!openRepos.containsKey(project),
"repo already opened: %s", project);
- } else {
- if (openRepos.containsKey(project)) {
- return;
- }
}
+
+ if (openRepos.containsKey(project)) {
+ return openRepos.get(project);
+ }
+
ProjectState projectState = projectCache.get(project);
if (projectState == null) {
throw new NoSuchProjectException(project);
@@ -203,6 +204,7 @@
OpenRepo or =
new OpenRepo(repoManager.openRepository(project), projectState);
openRepos.put(project, or);
+ return or;
} catch (RepositoryNotFoundException e) {
throw new NoSuchProjectException(project);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
index d7e8446..5a3b4ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.git;
/** Indicates the gitlink's update cannot be processed at this time. */
-class SubmoduleException extends Exception {
+public class SubmoduleException extends Exception {
private static final long serialVersionUID = 1L;
SubmoduleException(final String msg) {
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 1a8c4a7..7f79a68 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
@@ -15,6 +15,7 @@
package com.google.gerrit.server.git;
import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
import com.google.gerrit.common.data.SubscribeSection;
@@ -27,7 +28,6 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.BatchUpdate.Listener;
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
-import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
@@ -45,10 +45,8 @@
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
@@ -57,11 +55,15 @@
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
public class SubmoduleOp {
@@ -78,15 +80,17 @@
@Override
public void updateRepo(RepoContext ctx) throws Exception {
- CodeReviewCommit c = composeGitlinksCommit(branch, null);
- ctx.addRefUpdate(new ReceiveCommand(c.getParent(0), c, branch.get()));
- addBranchTip(branch, c);
+ CodeReviewCommit c = composeGitlinksCommit(branch);
+ if (c != null) {
+ ctx.addRefUpdate(new ReceiveCommand(c.getParent(0), c, branch.get()));
+ addBranchTip(branch, c);
+ }
}
}
public interface Factory {
SubmoduleOp create(
- Collection<Branch.NameKey> updatedBranches, MergeOpRepoManager orm);
+ Set<Branch.NameKey> updatedBranches, MergeOpRepoManager orm);
}
private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
@@ -98,10 +102,11 @@
private final boolean verboseSuperProject;
private final boolean enableSuperProjectSubscriptions;
private final Multimap<Branch.NameKey, SubmoduleSubscription> targets;
- private final Collection<Branch.NameKey> updatedBranches;
+ private final Set<Branch.NameKey> updatedBranches;
private final MergeOpRepoManager orm;
private final Map<Branch.NameKey, CodeReviewCommit> branchTips;
-
+ private final Map<Branch.NameKey, GitModules> branchGitModules;
+ private final ImmutableSet<Branch.NameKey> sortedBranches;
@AssistedInject
public SubmoduleOp(
@@ -110,7 +115,7 @@
@GerritServerConfig Config cfg,
ProjectCache projectCache,
ProjectState.Factory projectStateFactory,
- @Assisted Collection<Branch.NameKey> updatedBranches,
+ @Assisted Set<Branch.NameKey> updatedBranches,
@Assisted MergeOpRepoManager orm) throws SubmoduleException {
this.gitmodulesFactory = gitmodulesFactory;
this.myIdent = myIdent;
@@ -124,51 +129,93 @@
this.updatedBranches = updatedBranches;
this.targets = HashMultimap.create();
this.branchTips = new HashMap<>();
- calculateSubscriptionMap();
+ this.branchGitModules = new HashMap<>();
+ this.sortedBranches = calculateSubscriptionMap();
}
- private void calculateSubscriptionMap() throws SubmoduleException {
+ private ImmutableSet<Branch.NameKey> calculateSubscriptionMap()
+ throws SubmoduleException {
if (!enableSuperProjectSubscriptions) {
logDebug("Updating superprojects disabled");
- return;
+ return null;
}
logDebug("Calculating superprojects - submodules map");
+ LinkedHashSet<Branch.NameKey> allVisited = new LinkedHashSet<>();
for (Branch.NameKey updatedBranch : updatedBranches) {
- logDebug("Now processing " + updatedBranch);
- Set<Branch.NameKey> checkedTargets = new HashSet<>();
- Set<Branch.NameKey> targetsToProcess = new HashSet<>();
- targetsToProcess.add(updatedBranch);
+ if (allVisited.contains(updatedBranch)) {
+ continue;
+ }
- while (!targetsToProcess.isEmpty()) {
- Set<Branch.NameKey> newTargets = new HashSet<>();
- for (Branch.NameKey b : targetsToProcess) {
- try {
- Collection<SubmoduleSubscription> subs =
- superProjectSubscriptionsForSubmoduleBranch(b);
- for (SubmoduleSubscription sub : subs) {
- Branch.NameKey dst = sub.getSuperProject();
- targets.put(dst, sub);
- newTargets.add(dst);
- }
- } catch (IOException e) {
- throw new SubmoduleException("Cannot find superprojects for " + b, e);
- }
- }
- logDebug("adding to done " + targetsToProcess);
- checkedTargets.addAll(targetsToProcess);
- logDebug("completely done with " + checkedTargets);
+ searchForSuperprojects(updatedBranch, new LinkedHashSet<Branch.NameKey>(),
+ allVisited);
+ }
- Set<Branch.NameKey> intersection = new HashSet<>(checkedTargets);
- intersection.retainAll(newTargets);
- if (!intersection.isEmpty()) {
- throw new SubmoduleException(
- "Possible circular subscription involving " + updatedBranch);
- }
+ // Since the searchForSuperprojects will add the superprojects before one
+ // submodule in sortedBranches, need reverse the order of it
+ reverse(allVisited);
+ return ImmutableSet.copyOf(allVisited);
+ }
- targetsToProcess = newTargets;
+ private void searchForSuperprojects(Branch.NameKey current,
+ LinkedHashSet<Branch.NameKey> currentVisited,
+ LinkedHashSet<Branch.NameKey> allVisited)
+ throws SubmoduleException {
+ logDebug("Now processing " + current);
+
+ if (currentVisited.contains(current)) {
+ throw new SubmoduleException(
+ "Branch level circular subscriptions detected: " +
+ printCircularPath(currentVisited, current));
+ }
+
+ if (allVisited.contains(current)) {
+ return;
+ }
+
+ currentVisited.add(current);
+ try {
+ Collection<SubmoduleSubscription> subscriptions =
+ superProjectSubscriptionsForSubmoduleBranch(current);
+ for (SubmoduleSubscription sub : subscriptions) {
+ Branch.NameKey superProject = sub.getSuperProject();
+ searchForSuperprojects(superProject, currentVisited, allVisited);
+ targets.put(superProject, sub);
+ }
+ } catch (IOException e) {
+ throw new SubmoduleException("Cannot find superprojects for " + current,
+ e);
+ }
+ currentVisited.remove(current);
+ allVisited.add(current);
+ }
+
+ private static <T> void reverse(LinkedHashSet<T> set) {
+ if (set == null) {
+ return;
+ }
+
+ Deque<T> q = new ArrayDeque<>(set);
+ set.clear();
+
+ while (!q.isEmpty()) {
+ set.add(q.removeLast());
+ }
+ }
+
+ private <T> String printCircularPath(LinkedHashSet<T> p, T target) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(target);
+ ArrayList<T> reverseP = new ArrayList<>(p);
+ Collections.reverse(reverseP);
+ for (T t : reverseP) {
+ sb.append("->");
+ sb.append(t);
+ if (t.equals(target)) {
+ break;
}
}
+ return sb.toString();
}
private Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
@@ -180,14 +227,15 @@
if (r.matchSource(src.get())) {
if (r.getDestination() == null) {
// no need to care for wildcard, as we matched already
+ OpenRepo or;
try {
- orm.openRepo(s.getProject(), false);
+ or = orm.openRepo(s.getProject(), false);
} catch (NoSuchProjectException e) {
// A project listed a non existent project to be allowed
// to subscribe to it. Allow this for now.
continue;
}
- OpenRepo or = orm.getRepo(s.getProject());
+
for (Ref ref : or.repo.getRefDatabase().getRefs(
RefNames.REFS_HEADS).values()) {
ret.add(new Branch.NameKey(s.getProject(), ref.getName()));
@@ -222,8 +270,7 @@
for (Branch.NameKey targetBranch : branches) {
Project.NameKey targetProject = targetBranch.getParentKey();
try {
- orm.openRepo(targetProject, false);
- OpenRepo or = orm.getRepo(targetProject);
+ OpenRepo or = orm.openRepo(targetProject, false);
ObjectId id = or.repo.resolve(targetBranch.get());
if (id == null) {
logDebug("The branch " + targetBranch + " doesn't exist.");
@@ -233,14 +280,13 @@
logDebug("The project " + targetProject + " doesn't exist");
continue;
}
- GitModules m = gitmodulesFactory.create(targetBranch, orm);
- for (SubmoduleSubscription ss : m.subscribedTo(srcBranch)) {
- logDebug("Checking SubmoduleSubscription " + ss);
- if (projectCache.get(ss.getSubmodule().getParentKey()) != null) {
- logDebug("Adding SubmoduleSubscription " + ss);
- ret.add(ss);
- }
+
+ GitModules m = branchGitModules.get(targetBranch);
+ if (m == null) {
+ m = gitmodulesFactory.create(targetBranch, orm);
+ branchGitModules.put(targetBranch, m);
}
+ ret.addAll(m.subscribedTo(srcBranch));
}
}
logDebug("Calculated superprojects for " + srcBranch + " are " + ret);
@@ -248,20 +294,26 @@
}
public void updateSuperProjects() throws SubmoduleException {
+ ImmutableSet<Project.NameKey> projects = getProjectsInOrder();
+ if (projects == null) {
+ return;
+ }
+
SetMultimap<Project.NameKey, Branch.NameKey> dst = branchesByProject();
- Set<Project.NameKey> projects = dst.keySet();
+ LinkedHashSet<Project.NameKey> superProjects = new LinkedHashSet<>();
try {
for (Project.NameKey project : projects) {
- // get a new BatchUpdate for the project
- orm.openRepo(project, false);
- //TODO:czhen remove this when MergeOp combine this into BatchUpdate
- orm.getRepo(project).resetUpdate();
- for (Branch.NameKey branch : dst.get(project)) {
- SubmoduleOp.GitlinkOp op = new SubmoduleOp.GitlinkOp(branch);
- orm.getRepo(project).getUpdate().addRepoOnlyOp(op);
+ // only need superprojects
+ if (dst.containsKey(project)) {
+ superProjects.add(project);
+ // get a new BatchUpdate for the super project
+ OpenRepo or = orm.openRepo(project, false);
+ for (Branch.NameKey branch : dst.get(project)) {
+ addOp(or.getUpdate(), branch);
+ }
}
}
- BatchUpdate.execute(orm.batchUpdates(projects), Listener.NONE);
+ BatchUpdate.execute(orm.batchUpdates(superProjects), Listener.NONE);
} catch (RestApiException | UpdateException | IOException |
NoSuchProjectException e) {
throw new SubmoduleException("Cannot update gitlinks", e);
@@ -269,136 +321,178 @@
}
/**
- * Create a gitlink update commit on the tip of subscriber or modify the
- * baseCommit with gitlink update patch
+ * Create a separate gitlink commit
*/
- public CodeReviewCommit composeGitlinksCommit(
- final Branch.NameKey subscriber, RevCommit baseCommit)
+ public CodeReviewCommit composeGitlinksCommit(final Branch.NameKey subscriber)
throws IOException, SubmoduleException {
- PersonIdent author = null;
- StringBuilder msgbuf = new StringBuilder("Update git submodules\n\n");
- boolean sameAuthorForAll = true;
-
+ OpenRepo or;
try {
- orm.openRepo(subscriber.getParentKey(), false);
+ or = orm.openRepo(subscriber.getParentKey(), false);
} catch (NoSuchProjectException | IOException e) {
throw new SubmoduleException("Cannot access superproject", e);
}
- OpenRepo or = orm.getRepo(subscriber.getParentKey());
+ CodeReviewCommit currentCommit;
Ref r = or.repo.exactRef(subscriber.get());
if (r == null) {
throw new SubmoduleException(
"The branch was probably deleted from the subscriber repository");
}
+ currentCommit = or.rw.parseCommit(r.getObjectId());
- RevCommit currentCommit = (baseCommit != null) ? baseCommit :
- or.rw.parseCommit(or.repo.exactRef(subscriber.get()).getObjectId());
- or.rw.parseBody(currentCommit);
-
+ StringBuilder msgbuf = new StringBuilder("");
+ PersonIdent author = null;
DirCache dc = readTree(or.rw, currentCommit);
DirCacheEditor ed = dc.editor();
-
for (SubmoduleSubscription s : targets.get(subscriber)) {
- try {
- orm.openRepo(s.getSubmodule().getParentKey(), false);
- } catch (NoSuchProjectException | IOException e) {
- throw new SubmoduleException("Cannot access submodule", e);
- }
- OpenRepo subOr = orm.getRepo(s.getSubmodule().getParentKey());
- Repository subRepo = subOr.repo;
-
- Ref ref = subRepo.getRefDatabase().exactRef(s.getSubmodule().get());
- if (ref == null) {
- ed.add(new DeletePath(s.getPath()));
- continue;
- }
-
- ObjectId updateTo = ref.getObjectId();
- if (branchTips.containsKey(s.getSubmodule())) {
- updateTo = branchTips.get(s.getSubmodule());
- }
- RevWalk subOrRw = subOr.rw;
- final RevCommit newCommit = subOrRw.parseCommit(updateTo);
-
- subOrRw.parseBody(newCommit);
- if (author == null) {
- author = newCommit.getAuthorIdent();
- } else if (!author.equals(newCommit.getAuthorIdent())) {
- sameAuthorForAll = false;
- }
-
- DirCacheEntry dce = dc.getEntry(s.getPath());
- ObjectId oldId;
- if (dce != null) {
- if (!dce.getFileMode().equals(FileMode.GITLINK)) {
- String errMsg = "Requested to update gitlink " + s.getPath() + " in "
- + s.getSubmodule().getParentKey().get() + " but entry "
- + "doesn't have gitlink file mode.";
- throw new SubmoduleException(errMsg);
- }
- oldId = dce.getObjectId();
- } else {
- // This submodule did not exist before. We do not want to add
- // the full submodule history to the commit message, so omit it.
- oldId = updateTo;
- }
-
- ed.add(new PathEdit(s.getPath()) {
- @Override
- public void apply(DirCacheEntry ent) {
- ent.setFileMode(FileMode.GITLINK);
- ent.setObjectId(newCommit.getId());
- }
- });
- if (verboseSuperProject) {
- msgbuf.append("Project: " + s.getSubmodule().getParentKey().get());
- msgbuf.append(" " + s.getSubmodule().getShortName());
- msgbuf.append(" " + newCommit.getName());
- msgbuf.append("\n\n");
-
- try {
- subOrRw.resetRetain(subOr.canMergeFlag);
- subOrRw.markStart(newCommit);
- subOrRw.markUninteresting(subOrRw.parseCommit(oldId));
- for (RevCommit c : subOrRw) {
- subOrRw.parseBody(c);
- msgbuf.append(c.getFullMessage() + "\n\n");
- }
- } catch (IOException e) {
- throw new SubmoduleException("Could not perform a revwalk to "
- + "create superproject commit message", e);
+ RevCommit newCommit = updateSubmodule(dc, ed, msgbuf, s);
+ if (newCommit != null) {
+ if (author == null) {
+ author = newCommit.getAuthorIdent();
+ } else if (!author.equals(newCommit.getAuthorIdent())) {
+ author = myIdent;
}
}
}
ed.finish();
+ ObjectId newTreeId = dc.writeTree(or.ins);
-
- ObjectInserter oi = or.ins;
- CodeReviewRevWalk rw = or.rw;
- ObjectId tree = dc.writeTree(oi);
-
- if (!sameAuthorForAll || author == null) {
- author = myIdent;
+ // Gitlinks are already in the branch, return null
+ if (newTreeId.equals(currentCommit.getTree())) {
+ return null;
}
-
CommitBuilder commit = new CommitBuilder();
- commit.setTreeId(tree);
- if (baseCommit != null) {
- // modify the baseCommit
- commit.setParentIds(baseCommit.getParents());
- commit.setMessage(baseCommit.getFullMessage() + "\n\n" + msgbuf.toString());
- commit.setAuthor(baseCommit.getAuthorIdent());
- } else {
- // create a new commit
- commit.setParentId(currentCommit);
- commit.setMessage(msgbuf.toString());
- commit.setAuthor(author);
+ commit.setTreeId(newTreeId);
+ commit.setParentId(currentCommit);
+ StringBuilder commitMsg = new StringBuilder("Update git submodules\n\n");
+ if (verboseSuperProject) {
+ commitMsg.append(msgbuf);
}
+ commit.setMessage(commitMsg.toString());
+ commit.setAuthor(author);
commit.setCommitter(myIdent);
+ ObjectId id = or.ins.insert(commit);
+ return or.rw.parseCommit(id);
+ }
- ObjectId id = oi.insert(commit);
- return rw.parseCommit(id);
+ /**
+ * Amend an existing commit with gitlink updates
+ */
+ public CodeReviewCommit composeGitlinksCommit(
+ final Branch.NameKey subscriber, CodeReviewCommit currentCommit)
+ throws IOException, SubmoduleException {
+ OpenRepo or;
+ try {
+ or = orm.openRepo(subscriber.getParentKey(), false);
+ } catch (NoSuchProjectException | IOException e) {
+ throw new SubmoduleException("Cannot access superproject", e);
+ }
+
+ StringBuilder msgbuf = new StringBuilder("");
+ DirCache dc = readTree(or.rw, currentCommit);
+ DirCacheEditor ed = dc.editor();
+ for (SubmoduleSubscription s : targets.get(subscriber)) {
+ updateSubmodule(dc, ed, msgbuf, s);
+ }
+ ed.finish();
+ ObjectId newTreeId = dc.writeTree(or.ins);
+
+ // Gitlinks are already updated, just return the commit
+ if (newTreeId.equals(currentCommit.getTree())) {
+ return currentCommit;
+ }
+ or.rw.parseBody(currentCommit);
+ CommitBuilder commit = new CommitBuilder();
+ commit.setTreeId(newTreeId);
+ commit.setParentIds(currentCommit.getParents());
+ if (verboseSuperProject) {
+ commit.setMessage(
+ currentCommit.getFullMessage() + "\n\n*submodules:\n" + msgbuf.toString());
+ } else {
+ commit.setMessage(currentCommit.getFullMessage());
+ }
+ commit.setAuthor(currentCommit.getAuthorIdent());
+ commit.setCommitter(myIdent);
+ ObjectId id = or.ins.insert(commit);
+ return or.rw.parseCommit(id);
+ }
+
+ private RevCommit updateSubmodule(DirCache dc, DirCacheEditor ed,
+ StringBuilder msgbuf, final SubmoduleSubscription s)
+ throws SubmoduleException, IOException {
+ OpenRepo subOr;
+ try {
+ subOr = orm.openRepo(s.getSubmodule().getParentKey(), false);
+ } catch (NoSuchProjectException | IOException e) {
+ throw new SubmoduleException("Cannot access submodule", e);
+ }
+
+ DirCacheEntry dce = dc.getEntry(s.getPath());
+ RevCommit oldCommit = null;
+ if (dce != null) {
+ if (!dce.getFileMode().equals(FileMode.GITLINK)) {
+ String errMsg = "Requested to update gitlink " + s.getPath() + " in "
+ + s.getSubmodule().getParentKey().get() + " but entry "
+ + "doesn't have gitlink file mode.";
+ throw new SubmoduleException(errMsg);
+ }
+ oldCommit = subOr.rw.parseCommit(dce.getObjectId());
+ }
+
+ final RevCommit newCommit;
+ if (branchTips.containsKey(s.getSubmodule())) {
+ newCommit = branchTips.get(s.getSubmodule());
+ } else {
+ Ref ref = subOr.repo.getRefDatabase().exactRef(s.getSubmodule().get());
+ if (ref == null) {
+ ed.add(new DeletePath(s.getPath()));
+ return null;
+ }
+ newCommit = subOr.rw.parseCommit(ref.getObjectId());
+ }
+
+ if (Objects.equals(newCommit, oldCommit)) {
+ // gitlink have already been updated for this submodule
+ return null;
+ }
+ ed.add(new PathEdit(s.getPath()) {
+ @Override
+ public void apply(DirCacheEntry ent) {
+ ent.setFileMode(FileMode.GITLINK);
+ ent.setObjectId(newCommit.getId());
+ }
+ });
+
+ if (verboseSuperProject) {
+ createSubmoduleCommitMsg(msgbuf, s, subOr, newCommit, oldCommit);
+ }
+ subOr.rw.parseBody(newCommit);
+ return newCommit;
+ }
+
+ private void createSubmoduleCommitMsg(StringBuilder msgbuf,
+ SubmoduleSubscription s, OpenRepo subOr, RevCommit newCommit, RevCommit oldCommit)
+ throws SubmoduleException {
+ msgbuf.append("* Update " + s.getPath());
+ msgbuf.append(" from branch '" + s.getSubmodule().getShortName() + "'");
+
+ // newly created submodule gitlink, do not append whole history
+ if (oldCommit == null) {
+ return;
+ }
+
+ try {
+ subOr.rw.resetRetain(subOr.canMergeFlag);
+ subOr.rw.markStart(newCommit);
+ subOr.rw.markUninteresting(oldCommit);
+ for (RevCommit c : subOr.rw) {
+ subOr.rw.parseBody(c);
+ msgbuf.append("\n - " + c.getFullMessage().replace("\n", "\n "));
+ }
+ } catch (IOException e) {
+ throw new SubmoduleException("Could not perform a revwalk to "
+ + "create superproject commit message", e);
+ }
}
private static DirCache readTree(RevWalk rw, ObjectId base)
@@ -421,10 +515,46 @@
return ret;
}
+ public ImmutableSet<Project.NameKey> getProjectsInOrder()
+ throws SubmoduleException {
+ if (sortedBranches == null) {
+ return null;
+ }
+
+ LinkedHashSet<Project.NameKey> projects = new LinkedHashSet<>();
+ Project.NameKey prev = null;
+ for (Branch.NameKey branch : sortedBranches) {
+ Project.NameKey project = branch.getParentKey();
+ if (!project.equals(prev)) {
+ if (projects.contains(project)) {
+ throw new SubmoduleException(
+ "Project level circular subscriptions detected: " +
+ printCircularPath(projects, project));
+ }
+ projects.add(project);
+ }
+ prev = project;
+ }
+
+ return ImmutableSet.copyOf(projects);
+ }
+
+ public ImmutableSet<Branch.NameKey> getBranchesInOrder() {
+ return sortedBranches;
+ }
+
+ public boolean hasSubscription(Branch.NameKey branch) {
+ return targets.containsKey(branch);
+ }
+
public void addBranchTip(Branch.NameKey branch, CodeReviewCommit tip) {
branchTips.put(branch, tip);
}
+ public void addOp(BatchUpdate bu, Branch.NameKey branch) {
+ bu.addRepoOnlyOp(new GitlinkOp(branch));
+ }
+
private void logDebug(String msg, Object... args) {
if (log.isDebugEnabled()) {
log.debug("[" + orm.getSubmissionId() + "]" + msg, args);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 91d2cc7..4a5e94d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -74,11 +74,12 @@
}
@Override
- protected void updateRepoImpl(RepoContext ctx) {
+ protected void updateRepoImpl(RepoContext ctx) throws IntegrationException {
// The branch is unborn. Take fast-forward resolution to create the
// branch.
- args.mergeTip.moveTipTo(toMerge, toMerge);
- toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
+ CodeReviewCommit newCommit = amendGitlink(toMerge);
+ args.mergeTip.moveTipTo(newCommit, toMerge);
+ newCommit.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
}
}
@@ -105,7 +106,8 @@
}
@Override
- protected void updateRepoImpl(RepoContext ctx) throws IOException {
+ protected void updateRepoImpl(RepoContext ctx)
+ throws IntegrationException, IOException {
// If there is only one parent, a cherry-pick can be done by taking the
// delta relative to that one parent and redoing that on the current merge
// tip.
@@ -132,6 +134,7 @@
}
// Initial copy doesn't have new patch set ID since change hasn't been
// updated yet.
+ newCommit = amendGitlink(newCommit);
newCommit.copyFrom(toMerge);
newCommit.setPatchsetId(psId);
newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
@@ -189,12 +192,13 @@
// was configured.
MergeTip mergeTip = args.mergeTip;
if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
- mergeTip.moveTipTo(toMerge, toMerge);
+ mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
} else {
PersonIdent myIdent = new PersonIdent(args.serverIdent, ctx.getWhen());
CodeReviewCommit result = args.mergeUtil.mergeOneCommit(myIdent,
myIdent, args.repo, args.rw, args.inserter, args.destBranch,
mergeTip.getCurrentTip(), toMerge);
+ result = amendGitlink(result);
mergeTip.moveTipTo(result, toMerge);
}
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
index cd99daf..0e69128 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
@@ -18,16 +18,13 @@
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.IntegrationException;
-import java.io.IOException;
-
class FastForwardOp extends SubmitStrategyOp {
FastForwardOp(SubmitStrategy.Arguments args, CodeReviewCommit toMerge) {
super(args, toMerge);
}
@Override
- public void updateRepoImpl(RepoContext ctx)
- throws IntegrationException, IOException {
- args.mergeTip.moveTipTo(toMerge, toMerge);
+ protected void updateRepoImpl(RepoContext ctx) throws IntegrationException {
+ args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
index ab16647..b1590bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
@@ -44,6 +44,6 @@
args.mergeUtil.mergeOneCommit(caller, args.serverIdent,
ctx.getRepository(), args.rw, ctx.getInserter(), args.destBranch,
args.mergeTip.getCurrentTip(), toMerge);
- args.mergeTip.moveTipTo(merged, toMerge);
+ args.mergeTip.moveTipTo(amendGitlink(merged), toMerge);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index af1e577..cca7452 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -71,11 +71,12 @@
}
@Override
- public void updateRepoImpl(RepoContext ctx) {
+ public void updateRepoImpl(RepoContext ctx) throws IntegrationException {
// The branch is unborn. Take fast-forward resolution to create the
// branch.
toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
- args.mergeTip.moveTipTo(toMerge, toMerge);
+ CodeReviewCommit newCommit = amendGitlink(toMerge);
+ args.mergeTip.moveTipTo(newCommit, toMerge);
acceptMergeTip(args.mergeTip);
}
}
@@ -110,8 +111,7 @@
// BatchUpdate how to produce CodeReviewRevWalks.
if (args.mergeUtil.canFastForward(args.mergeSorter,
args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
- toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
- args.mergeTip.moveTipTo(toMerge, toMerge);
+ args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
acceptMergeTip(args.mergeTip);
return;
}
@@ -134,6 +134,7 @@
"Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
}
newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit());
+ newCommit = amendGitlink(newCommit);
newCommit.copyFrom(toMerge);
newCommit.setStatusCode(CommitMergeStatus.CLEAN_REBASE);
newCommit.setPatchsetId(rebaseOp.getPatchSetId());
@@ -182,13 +183,13 @@
// configured.
MergeTip mergeTip = args.mergeTip;
if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
- mergeTip.moveTipTo(toMerge, toMerge);
+ mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
acceptMergeTip(mergeTip);
} else {
CodeReviewCommit newTip = args.mergeUtil.mergeOneCommit(
args.serverIdent, args.serverIdent, args.repo, args.rw,
args.inserter, args.destBranch, mergeTip.getCurrentTip(), toMerge);
- mergeTip.moveTipTo(newTip, toMerge);
+ mergeTip.moveTipTo(amendGitlink(newTip), toMerge);
}
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
mergeTip.getCurrentTip(), args.alreadyAccepted);
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 c0d9d2d..3761660 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
@@ -41,6 +41,7 @@
import com.google.gerrit.server.git.MergeSorter;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.SubmoduleOp;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ChangeControl;
@@ -93,7 +94,8 @@
ReviewDb db,
Set<RevCommit> alreadyAccepted,
String submissionId,
- NotifyHandling notifyHandling);
+ NotifyHandling notifyHandling,
+ SubmoduleOp submoduleOp);
}
final AccountCache accountCache;
@@ -125,6 +127,7 @@
final String submissionId;
final SubmitType submitType;
final NotifyHandling notifyHandling;
+ final SubmoduleOp submoduleOp;
final ProjectState project;
final MergeSorter mergeSorter;
@@ -160,7 +163,8 @@
@Assisted Set<RevCommit> alreadyAccepted,
@Assisted String submissionId,
@Assisted SubmitType submitType,
- @Assisted NotifyHandling notifyHandling) {
+ @Assisted NotifyHandling notifyHandling,
+ @Assisted SubmoduleOp submoduleOp) {
this.accountCache = accountCache;
this.approvalsUtil = approvalsUtil;
this.batchUpdateFactory = batchUpdateFactory;
@@ -190,6 +194,7 @@
this.submissionId = submissionId;
this.submitType = submitType;
this.notifyHandling = notifyHandling;
+ this.submoduleOp = submoduleOp;
this.project = checkNotNull(projectCache.get(destBranch.getParentKey()),
"project not found: %s", destBranch.getParentKey());
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 725a1b4..ee49eae 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
@@ -23,6 +23,7 @@
import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.MergeOp.CommitStatus;
import com.google.gerrit.server.git.MergeTip;
+import com.google.gerrit.server.git.SubmoduleOp;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -52,11 +53,12 @@
Repository repo, CodeReviewRevWalk rw, ObjectInserter inserter,
RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted,
Branch.NameKey destBranch, IdentifiedUser caller, MergeTip mergeTip,
- CommitStatus commits, String submissionId, NotifyHandling notifyHandling)
+ CommitStatus commits, String submissionId, NotifyHandling notifyHandling,
+ SubmoduleOp submoduleOp)
throws IntegrationException {
SubmitStrategy.Arguments args = argsFactory.create(submitType, destBranch,
commits, rw, caller, mergeTip, inserter, repo, canMergeFlag, db,
- alreadyAccepted, submissionId, notifyHandling);
+ alreadyAccepted, submissionId, notifyHandling, submoduleOp);
switch (submitType) {
case CHERRY_PICK:
return new CherryPick(args);
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 788bcb3..df29372 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
@@ -46,6 +46,7 @@
import com.google.gerrit.server.git.LabelNormalizer;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.SubmoduleException;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.project.ProjectState;
import com.google.gwtorm.server.OrmException;
@@ -136,6 +137,7 @@
tipAfter,
getDest().get());
ctx.addRefUpdate(command);
+ args.submoduleOp.addBranchTip(getDest(), tipAfter);
}
private void checkProjectConfig(RepoContext ctx, CodeReviewCommit commit)
@@ -556,6 +558,34 @@
protected void postUpdateImpl(Context ctx) throws Exception {
}
+ /**
+ * Amend the commit with gitlink update
+ * @param commit
+ */
+ protected CodeReviewCommit amendGitlink(CodeReviewCommit commit)
+ throws IntegrationException {
+ CodeReviewCommit newCommit = commit;
+ // Modify the commit with gitlink update
+ if (args.submoduleOp.hasSubscription(args.destBranch)) {
+ try {
+ newCommit =
+ args.submoduleOp.composeGitlinksCommit(args.destBranch, commit);
+ newCommit.copyFrom(commit);
+ if (commit.equals(toMerge)) {
+ newCommit.setPatchsetId(ChangeUtil.nextPatchSetId(
+ args.repo, toMerge.change().currentPatchSetId()));
+ args.commits.put(newCommit);
+ }
+ } catch (SubmoduleException | IOException e) {
+ throw new IntegrationException(
+ "cannot update gitlink for the commit at branch: "
+ + args.destBranch);
+ }
+ }
+
+ return newCommit;
+ }
+
protected final void logDebug(String msg, Object... args) {
if (log.isDebugEnabled()) {
log.debug("[" + this.args.submissionId + "]" + msg, args);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index cdaf08c..627fe66 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -584,7 +584,8 @@
//
// Parse notes from the staged result so we can return something useful
// to the caller instead of throwing.
- log.debug("Rebuilding change {} failed", getChangeId());
+ log.debug("Rebuilding change {} failed: {}",
+ getChangeId(), e.getMessage());
args.metrics.autoRebuildFailureCount.increment(CHANGES);
rebuildResult = checkNotNull(r);
checkNotNull(r.newState());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
index 8130913..30c6d10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
@@ -176,6 +176,16 @@
}
}
+ private static class ConflictingUpdateException extends OrmRuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ ConflictingUpdateException(Change change, String expectedNoteDbState) {
+ super(String.format(
+ "Expected change %s to have noteDbState %s but was %s",
+ change.getId(), expectedNoteDbState, change.getNoteDbState()));
+ }
+ }
+
@Override
public Result rebuild(NoteDbUpdateManager manager,
ChangeBundle bundle) throws NoSuchChangeException, IOException,
@@ -217,8 +227,13 @@
db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
- if (!Objects.equals(oldNoteDbState, change.getNoteDbState())) {
+ String currNoteDbState = change.getNoteDbState();
+ if (Objects.equals(currNoteDbState, newNoteDbState)) {
+ // Another thread completed the same rebuild we were about to.
throw new AbortUpdateException();
+ } else if (!Objects.equals(oldNoteDbState, currNoteDbState)) {
+ // Another thread updated the state to something else.
+ throw new ConflictingUpdateException(change, oldNoteDbState);
}
change.setNoteDbState(newNoteDbState);
return change;
@@ -233,7 +248,15 @@
throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
}
} catch (AbortUpdateException e) {
- // Drop this rebuild; another thread completed it.
+ // Drop this rebuild; another thread completed it. It's ok to not execute
+ // the update in this case, since the object referenced in the Result was
+ // flushed to the repo by whatever thread won the race.
+ } catch (ConflictingUpdateException e) {
+ // Rethrow as an OrmException so the caller knows to use staged results.
+ // Strictly speaking they are not completely up to date, but result we
+ // send to the caller is the same as if this rebuild had executed before
+ // the other thread.
+ throw new OrmException(e.getMessage());
}
return r;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 684e4f6..08195e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -206,7 +206,8 @@
repo.scanForRepoChanges();
} catch (OrmException | IOException e) {
// See ChangeNotes#rebuildAndOpen.
- log.debug("Rebuilding change {} via drafts failed", getChangeId());
+ log.debug("Rebuilding change {} via drafts failed: {}",
+ getChangeId(), e.getMessage());
args.metrics.autoRebuildFailureCount.increment(CHANGES);
checkNotNull(r.staged());
return LoadHandle.create(