Fix submittabilty of merge commits that resolve conflicts
Change implementation of 2.12 fix [1] to take into account note-db. [1] https://gerrit-review.googlesource.com/#/c/74461/ Bug: Issue 3811 Change-Id: Iafd5986ce20449dd380f6d274fbd9cb30d1cdd98
This commit is contained in:
		 Alexandre Philbert
					Alexandre Philbert
				
			
				
					committed by
					
						 David Pursehouse
						David Pursehouse
					
				
			
			
				
	
			
			
			 David Pursehouse
						David Pursehouse
					
				
			
						parent
						
							44d9c9926a
						
					
				
				
					commit
					10f39fb518
				
			| @@ -0,0 +1,363 @@ | |||||||
|  | // Copyright (C) 2016 The Android Open Source Project | ||||||
|  | // | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | // you may not use this file except in compliance with the License. | ||||||
|  | // You may obtain a copy of the License at | ||||||
|  | // | ||||||
|  | // http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  | // | ||||||
|  | // Unless required by applicable law or agreed to in writing, software | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | // See the License for the specific language governing permissions and | ||||||
|  | // limitations under the License. | ||||||
|  |  | ||||||
|  | package com.google.gerrit.acceptance.rest.change; | ||||||
|  |  | ||||||
|  | import static com.google.common.truth.Truth.assertThat; | ||||||
|  | import static com.google.common.truth.TruthJUnit.assume; | ||||||
|  |  | ||||||
|  | import com.google.common.collect.ImmutableList; | ||||||
|  | import com.google.gerrit.acceptance.AbstractDaemonTest; | ||||||
|  | import com.google.gerrit.acceptance.NoHttpd; | ||||||
|  | import com.google.gerrit.acceptance.PushOneCommit; | ||||||
|  | import com.google.gerrit.extensions.client.ChangeStatus; | ||||||
|  | import com.google.gerrit.reviewdb.client.Project; | ||||||
|  | import com.google.gerrit.server.change.Submit; | ||||||
|  | import com.google.gerrit.server.git.ChangeSet; | ||||||
|  | import com.google.gerrit.server.git.MergeSuperSet; | ||||||
|  | import com.google.gerrit.server.query.change.ChangeData; | ||||||
|  | import com.google.gerrit.testutil.ConfigSuite; | ||||||
|  | import com.google.gwtorm.server.OrmException; | ||||||
|  | import com.google.inject.Inject; | ||||||
|  |  | ||||||
|  | import org.eclipse.jgit.errors.IncorrectObjectTypeException; | ||||||
|  | import org.eclipse.jgit.errors.MissingObjectException; | ||||||
|  | import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; | ||||||
|  | import org.eclipse.jgit.junit.TestRepository; | ||||||
|  | import org.eclipse.jgit.lib.Config; | ||||||
|  | import org.eclipse.jgit.revwalk.RevCommit; | ||||||
|  | import org.junit.Test; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | @NoHttpd | ||||||
|  | public class SubmitResolvingMergeCommitIT extends AbstractDaemonTest { | ||||||
|  |   @Inject | ||||||
|  |   private MergeSuperSet mergeSuperSet; | ||||||
|  |  | ||||||
|  |   @Inject | ||||||
|  |   private Submit submit; | ||||||
|  |  | ||||||
|  |   @ConfigSuite.Default | ||||||
|  |   public static Config submitWholeTopicEnabled() { | ||||||
|  |     return submitWholeTopicEnabledConfig(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @Test | ||||||
|  |   public void resolvingMergeCommitAtEndOfChain() throws Exception { | ||||||
|  |     /* | ||||||
|  |       A <- B <- C <------- D | ||||||
|  |       ^                    ^ | ||||||
|  |       |                    | | ||||||
|  |       E <- F <- G <- H <-- M* | ||||||
|  |  | ||||||
|  |       G has a conflict with C and is resolved in M which is a merge | ||||||
|  |       commit of H and D. | ||||||
|  |     */ | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result a = createChange("A"); | ||||||
|  |     PushOneCommit.Result b = createChange("B", "new.txt", "No conflict line", | ||||||
|  |         ImmutableList.of(a.getCommit())); | ||||||
|  |     PushOneCommit.Result c = createChange("C", ImmutableList.of(b.getCommit())); | ||||||
|  |     PushOneCommit.Result d = createChange("D", ImmutableList.of(c.getCommit())); | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result e = createChange("E", ImmutableList.of(a.getCommit())); | ||||||
|  |     PushOneCommit.Result f = createChange("F", ImmutableList.of(e.getCommit())); | ||||||
|  |     PushOneCommit.Result g = createChange("G", "new.txt", "Conflicting line", | ||||||
|  |         ImmutableList.of(f.getCommit())); | ||||||
|  |     PushOneCommit.Result h = createChange("H", ImmutableList.of(g.getCommit())); | ||||||
|  |  | ||||||
|  |     approve(a.getChangeId()); | ||||||
|  |     approve(b.getChangeId()); | ||||||
|  |     approve(c.getChangeId()); | ||||||
|  |     approve(d.getChangeId()); | ||||||
|  |     submit(d.getChangeId()); | ||||||
|  |  | ||||||
|  |     approve(e.getChangeId()); | ||||||
|  |     approve(f.getChangeId()); | ||||||
|  |     approve(g.getChangeId()); | ||||||
|  |     approve(h.getChangeId()); | ||||||
|  |  | ||||||
|  |     assertMergeable(e.getChange(), true); | ||||||
|  |     assertMergeable(f.getChange(), true); | ||||||
|  |     assertMergeable(g.getChange(), false); | ||||||
|  |     assertMergeable(h.getChange(), false); | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result m = createChange("M", "new.txt", "Resolved conflict", | ||||||
|  |         ImmutableList.of(d.getCommit(), h.getCommit())); | ||||||
|  |     approve(m.getChangeId()); | ||||||
|  |  | ||||||
|  |     assertChangeSetMergeable(m.getChange(), true); | ||||||
|  |  | ||||||
|  |     assertMergeable(m.getChange(), true); | ||||||
|  |     submit(m.getChangeId()); | ||||||
|  |  | ||||||
|  |     assertMerged(e.getChangeId()); | ||||||
|  |     assertMerged(f.getChangeId()); | ||||||
|  |     assertMerged(g.getChangeId()); | ||||||
|  |     assertMerged(h.getChangeId()); | ||||||
|  |     assertMerged(m.getChangeId()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @Test | ||||||
|  |   public void resolvingMergeCommitComingBeforeConflict() throws Exception { | ||||||
|  |     /* | ||||||
|  |       A <- B <- C <- D | ||||||
|  |       ^    ^ | ||||||
|  |       |    | | ||||||
|  |       E <- F* <- G | ||||||
|  |  | ||||||
|  |       F is a merge commit of E and B and resolves any conflict. | ||||||
|  |       However G is conflicting with C. | ||||||
|  |     */ | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result a = createChange("A"); | ||||||
|  |     PushOneCommit.Result b = createChange("B", "new.txt", "No conflict line", | ||||||
|  |         ImmutableList.of(a.getCommit())); | ||||||
|  |     PushOneCommit.Result c = createChange("C", "new.txt", "No conflict line #2", | ||||||
|  |         ImmutableList.of(b.getCommit())); | ||||||
|  |     PushOneCommit.Result d = createChange("D", ImmutableList.of(c.getCommit())); | ||||||
|  |     PushOneCommit.Result e = createChange("E", "new.txt", "Conflicting line", | ||||||
|  |         ImmutableList.of(a.getCommit())); | ||||||
|  |     PushOneCommit.Result f = createChange("F", "new.txt", "Resolved conflict", | ||||||
|  |         ImmutableList.of(b.getCommit(), e.getCommit())); | ||||||
|  |     PushOneCommit.Result g = createChange("G", "new.txt", "Conflicting line #2", | ||||||
|  |         ImmutableList.of(f.getCommit())); | ||||||
|  |  | ||||||
|  |     assertMergeable(e.getChange(), true); | ||||||
|  |  | ||||||
|  |     approve(a.getChangeId()); | ||||||
|  |     approve(b.getChangeId()); | ||||||
|  |     submit(b.getChangeId()); | ||||||
|  |  | ||||||
|  |     assertMergeable(e.getChange(), false); | ||||||
|  |     assertMergeable(f.getChange(), true); | ||||||
|  |     assertMergeable(g.getChange(), true); | ||||||
|  |  | ||||||
|  |     approve(c.getChangeId()); | ||||||
|  |     approve(d.getChangeId()); | ||||||
|  |     submit(d.getChangeId()); | ||||||
|  |  | ||||||
|  |     approve(e.getChangeId()); | ||||||
|  |     approve(f.getChangeId()); | ||||||
|  |     approve(g.getChangeId()); | ||||||
|  |  | ||||||
|  |     assertMergeable(g.getChange(), false); | ||||||
|  |     assertChangeSetMergeable(g.getChange(), false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @Test | ||||||
|  |   public void resolvingMergeCommitWithTopics() throws Exception { | ||||||
|  |     /* | ||||||
|  |       Project1: | ||||||
|  |         A <- B <-- C <--- | ||||||
|  |         ^    ^          | | ||||||
|  |         |    |          | | ||||||
|  |         E <- F* <- G <- L* | ||||||
|  |  | ||||||
|  |       G clashes with C, and F resolves the clashes between E and B. | ||||||
|  |       Later, L resolves the clashes between C and G. | ||||||
|  |  | ||||||
|  |       Project2: | ||||||
|  |         H <- I | ||||||
|  |         ^    ^ | ||||||
|  |         |    | | ||||||
|  |         J <- K* | ||||||
|  |  | ||||||
|  |       J clashes with I, and K resolves all problems. | ||||||
|  |       G, K and L are in the same topic. | ||||||
|  |     */ | ||||||
|  |     assume().that(isSubmitWholeTopicEnabled()).isTrue(); | ||||||
|  |  | ||||||
|  |     String project1Name = name("Project1"); | ||||||
|  |     String project2Name = name("Project2"); | ||||||
|  |     gApi.projects().create(project1Name); | ||||||
|  |     gApi.projects().create(project2Name); | ||||||
|  |     TestRepository<InMemoryRepository> project1 = | ||||||
|  |         cloneProject(new Project.NameKey(project1Name)); | ||||||
|  |     TestRepository<InMemoryRepository> project2 = | ||||||
|  |         cloneProject(new Project.NameKey(project2Name)); | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result a = createChange(project1, "A"); | ||||||
|  |     PushOneCommit.Result b = createChange(project1, "B", "new.txt", | ||||||
|  |         "No conflict line", ImmutableList.of(a.getCommit())); | ||||||
|  |     PushOneCommit.Result c = createChange(project1, "C", "new.txt", | ||||||
|  |         "No conflict line #2", ImmutableList.of(b.getCommit())); | ||||||
|  |  | ||||||
|  |     approve(a.getChangeId()); | ||||||
|  |     approve(b.getChangeId()); | ||||||
|  |     approve(c.getChangeId()); | ||||||
|  |     submit(c.getChangeId()); | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result e = createChange(project1, "E", "new.txt", | ||||||
|  |         "Conflicting line", ImmutableList.of(a.getCommit())); | ||||||
|  |     PushOneCommit.Result f = createChange(project1, "F", "new.txt", | ||||||
|  |         "Resolved conflict", ImmutableList.of(b.getCommit(), e.getCommit())); | ||||||
|  |     PushOneCommit.Result g = createChange(project1, "G", "new.txt", | ||||||
|  |         "Conflicting line #2", ImmutableList.of(f.getCommit()), | ||||||
|  |         "refs/for/master/" + name("topic1")); | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result h = createChange(project2, "H"); | ||||||
|  |     PushOneCommit.Result i = createChange(project2, "I", "new.txt", | ||||||
|  |         "No conflict line", ImmutableList.of(h.getCommit())); | ||||||
|  |     PushOneCommit.Result j = createChange(project2, "J", "new.txt", | ||||||
|  |         "Conflicting line", ImmutableList.of(h.getCommit())); | ||||||
|  |     PushOneCommit.Result k = | ||||||
|  |         createChange(project2, "K", "new.txt", "Sadly conflicting topic-wise", | ||||||
|  |             ImmutableList.of(i.getCommit(), j.getCommit()), | ||||||
|  |             "refs/for/master/" + name("topic1")); | ||||||
|  |  | ||||||
|  |     approve(h.getChangeId()); | ||||||
|  |     approve(i.getChangeId()); | ||||||
|  |     submit(i.getChangeId()); | ||||||
|  |  | ||||||
|  |     approve(e.getChangeId()); | ||||||
|  |     approve(f.getChangeId()); | ||||||
|  |     approve(g.getChangeId()); | ||||||
|  |     approve(j.getChangeId()); | ||||||
|  |     approve(k.getChangeId()); | ||||||
|  |  | ||||||
|  |     assertChangeSetMergeable(g.getChange(), false); | ||||||
|  |     assertChangeSetMergeable(k.getChange(), false); | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result l = | ||||||
|  |         createChange(project1, "L", "new.txt", "Resolving conflicts again", | ||||||
|  |             ImmutableList.of(c.getCommit(), g.getCommit()), | ||||||
|  |             "refs/for/master/" + name("topic1")); | ||||||
|  |  | ||||||
|  |     approve(l.getChangeId()); | ||||||
|  |     assertChangeSetMergeable(l.getChange(), true); | ||||||
|  |  | ||||||
|  |     submit(l.getChangeId()); | ||||||
|  |     assertMerged(c.getChangeId()); | ||||||
|  |     assertMerged(g.getChangeId()); | ||||||
|  |     assertMerged(k.getChangeId()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @Test | ||||||
|  |   public void resolvingMergeCommitAtEndOfChainAndNotUpToDate() throws Exception { | ||||||
|  |     /* | ||||||
|  |         A <-- B | ||||||
|  |          \ | ||||||
|  |           C  <- D | ||||||
|  |            \   / | ||||||
|  |              E | ||||||
|  |  | ||||||
|  |         B is the target branch, and D should be merged with B, but one | ||||||
|  |         of C conflicts with B | ||||||
|  |     */ | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result a = createChange("A"); | ||||||
|  |     PushOneCommit.Result b = createChange("B", "new.txt", "No conflict line", | ||||||
|  |         ImmutableList.of(a.getCommit())); | ||||||
|  |  | ||||||
|  |     approve(a.getChangeId()); | ||||||
|  |     approve(b.getChangeId()); | ||||||
|  |     submit(b.getChangeId()); | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result c = createChange("C", "new.txt", "Create conflicts", | ||||||
|  |         ImmutableList.of(a.getCommit())); | ||||||
|  |     PushOneCommit.Result e = createChange("E", ImmutableList.of(c.getCommit())); | ||||||
|  |     PushOneCommit.Result d = createChange("D", "new.txt", "Resolves conflicts", | ||||||
|  |         ImmutableList.of(c.getCommit(), e.getCommit())); | ||||||
|  |  | ||||||
|  |     approve(c.getChangeId()); | ||||||
|  |     approve(e.getChangeId()); | ||||||
|  |     approve(d.getChangeId()); | ||||||
|  |     assertMergeable(d.getChange(), false); | ||||||
|  |     assertChangeSetMergeable(d.getChange(), false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private void submit(String changeId) throws Exception { | ||||||
|  |     gApi.changes() | ||||||
|  |         .id(changeId) | ||||||
|  |         .current() | ||||||
|  |         .submit(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private void assertChangeSetMergeable(ChangeData change, boolean expected) | ||||||
|  |       throws MissingObjectException, IncorrectObjectTypeException, IOException, | ||||||
|  |       OrmException { | ||||||
|  |     ChangeSet cs = | ||||||
|  |         mergeSuperSet.completeChangeSet(db, change.change(), user(admin)); | ||||||
|  |     assertThat(submit.isPatchSetMergeable(cs)).isEqualTo(expected); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private void assertMergeable(ChangeData change, boolean expected) | ||||||
|  |       throws Exception { | ||||||
|  |     change.setMergeable(null); | ||||||
|  |     assertThat(change.isMergeable()).isEqualTo(expected); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private void assertMerged(String changeId) throws Exception { | ||||||
|  |     assertThat(gApi | ||||||
|  |         .changes() | ||||||
|  |         .id(changeId) | ||||||
|  |         .get() | ||||||
|  |         .status).isEqualTo(ChangeStatus.MERGED); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private PushOneCommit.Result createChange(TestRepository<?> repo, | ||||||
|  |       String subject, String fileName, String content, List<RevCommit> parents, | ||||||
|  |       String ref) throws Exception { | ||||||
|  |     PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo, | ||||||
|  |         subject, fileName, content); | ||||||
|  |  | ||||||
|  |     if (!parents.isEmpty()) { | ||||||
|  |       push.setParents(parents); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     PushOneCommit.Result result; | ||||||
|  |     if (fileName.isEmpty()) { | ||||||
|  |       result = push.execute(ref); | ||||||
|  |     } else { | ||||||
|  |       result = push.to(ref); | ||||||
|  |     } | ||||||
|  |     result.assertOkStatus(); | ||||||
|  |     return result; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private PushOneCommit.Result createChange(TestRepository<?> repo, | ||||||
|  |       String subject) throws Exception { | ||||||
|  |     return createChange(repo, subject, "x", "x", new ArrayList<RevCommit>(), | ||||||
|  |         "refs/for/master"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private PushOneCommit.Result createChange(TestRepository<?> repo, | ||||||
|  |       String subject, String fileName, String content, List<RevCommit> parents) | ||||||
|  |           throws Exception { | ||||||
|  |     return createChange(repo, subject, fileName, content, parents, | ||||||
|  |         "refs/for/master"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @Override | ||||||
|  |   protected PushOneCommit.Result createChange(String subject) throws Exception { | ||||||
|  |     return createChange(testRepo, subject, "", "", | ||||||
|  |         Collections.<RevCommit> emptyList(), "refs/for/master"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private PushOneCommit.Result createChange(String subject, | ||||||
|  |       List<RevCommit> parents) throws Exception { | ||||||
|  |     return createChange(testRepo, subject, "", "", parents, "refs/for/master"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private PushOneCommit.Result createChange(String subject, String fileName, | ||||||
|  |       String content, List<RevCommit> parents) throws Exception { | ||||||
|  |     return createChange(testRepo, subject, fileName, content, parents, | ||||||
|  |         "refs/for/master"); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -20,6 +20,8 @@ import com.google.common.base.Predicate; | |||||||
| import com.google.common.base.Strings; | import com.google.common.base.Strings; | ||||||
| import com.google.common.collect.FluentIterable; | import com.google.common.collect.FluentIterable; | ||||||
| import com.google.common.collect.ImmutableMap; | import com.google.common.collect.ImmutableMap; | ||||||
|  | import com.google.common.collect.Multimap; | ||||||
|  | import com.google.common.collect.Sets; | ||||||
| import com.google.gerrit.common.data.ParameterizedString; | import com.google.gerrit.common.data.ParameterizedString; | ||||||
| import com.google.gerrit.extensions.api.changes.SubmitInput; | import com.google.gerrit.extensions.api.changes.SubmitInput; | ||||||
| import com.google.gerrit.extensions.common.ChangeInfo; | import com.google.gerrit.extensions.common.ChangeInfo; | ||||||
| @@ -29,9 +31,11 @@ import com.google.gerrit.extensions.restapi.RestApiException; | |||||||
| import com.google.gerrit.extensions.restapi.RestModifyView; | import com.google.gerrit.extensions.restapi.RestModifyView; | ||||||
| import com.google.gerrit.extensions.restapi.UnprocessableEntityException; | import com.google.gerrit.extensions.restapi.UnprocessableEntityException; | ||||||
| import com.google.gerrit.extensions.webui.UiAction; | import com.google.gerrit.extensions.webui.UiAction; | ||||||
|  | import com.google.gerrit.reviewdb.client.Branch; | ||||||
| import com.google.gerrit.reviewdb.client.Change; | import com.google.gerrit.reviewdb.client.Change; | ||||||
| import com.google.gerrit.reviewdb.client.ChangeMessage; | import com.google.gerrit.reviewdb.client.ChangeMessage; | ||||||
| import com.google.gerrit.reviewdb.client.PatchSet; | import com.google.gerrit.reviewdb.client.PatchSet; | ||||||
|  | import com.google.gerrit.reviewdb.client.Project; | ||||||
| import com.google.gerrit.reviewdb.client.RevId; | import com.google.gerrit.reviewdb.client.RevId; | ||||||
| import com.google.gerrit.reviewdb.server.ReviewDb; | import com.google.gerrit.reviewdb.server.ReviewDb; | ||||||
| import com.google.gerrit.server.ChangeMessagesUtil; | import com.google.gerrit.server.ChangeMessagesUtil; | ||||||
| @@ -58,12 +62,18 @@ import com.google.inject.Singleton; | |||||||
| import org.eclipse.jgit.errors.RepositoryNotFoundException; | import org.eclipse.jgit.errors.RepositoryNotFoundException; | ||||||
| import org.eclipse.jgit.lib.Config; | import org.eclipse.jgit.lib.Config; | ||||||
| import org.eclipse.jgit.lib.ObjectId; | import org.eclipse.jgit.lib.ObjectId; | ||||||
|  | import org.eclipse.jgit.lib.Repository; | ||||||
|  | import org.eclipse.jgit.revwalk.RevCommit; | ||||||
|  | import org.eclipse.jgit.revwalk.RevWalk; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.HashMap; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
|  |  | ||||||
| @Singleton | @Singleton | ||||||
| public class Submit implements RestModifyView<RevisionResource, SubmitInput>, | public class Submit implements RestModifyView<RevisionResource, SubmitInput>, | ||||||
| @@ -129,6 +139,7 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>, | |||||||
|   private final ParameterizedString submitTopicTooltip; |   private final ParameterizedString submitTopicTooltip; | ||||||
|   private final boolean submitWholeTopic; |   private final boolean submitWholeTopic; | ||||||
|   private final Provider<InternalChangeQuery> queryProvider; |   private final Provider<InternalChangeQuery> queryProvider; | ||||||
|  |   private final PatchSetUtil psUtil; | ||||||
|  |  | ||||||
|   @Inject |   @Inject | ||||||
|   Submit(Provider<ReviewDb> dbProvider, |   Submit(Provider<ReviewDb> dbProvider, | ||||||
| @@ -141,7 +152,8 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>, | |||||||
|       AccountsCollection accounts, |       AccountsCollection accounts, | ||||||
|       ChangesCollection changes, |       ChangesCollection changes, | ||||||
|       @GerritServerConfig Config cfg, |       @GerritServerConfig Config cfg, | ||||||
|       Provider<InternalChangeQuery> queryProvider) { |       Provider<InternalChangeQuery> queryProvider, | ||||||
|  |       PatchSetUtil psUtil) { | ||||||
|     this.dbProvider = dbProvider; |     this.dbProvider = dbProvider; | ||||||
|     this.repoManager = repoManager; |     this.repoManager = repoManager; | ||||||
|     this.changeDataFactory = changeDataFactory; |     this.changeDataFactory = changeDataFactory; | ||||||
| @@ -173,6 +185,7 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>, | |||||||
|         cfg.getString("change", null, "submitTopicTooltip"), |         cfg.getString("change", null, "submitTopicTooltip"), | ||||||
|         DEFAULT_TOPIC_TOOLTIP)); |         DEFAULT_TOPIC_TOOLTIP)); | ||||||
|     this.queryProvider = queryProvider; |     this.queryProvider = queryProvider; | ||||||
|  |     this.psUtil = psUtil; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @Override |   @Override | ||||||
| @@ -247,24 +260,18 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>, | |||||||
|         if (!changeControl.canSubmit()) { |         if (!changeControl.canSubmit()) { | ||||||
|           return BLOCKED_SUBMIT_TOOLTIP; |           return BLOCKED_SUBMIT_TOOLTIP; | ||||||
|         } |         } | ||||||
|         // Recheck mergeability rather than using value stored in the index, |  | ||||||
|         // which may be stale. |  | ||||||
|         // TODO(dborowitz): This is ugly; consider providing a way to not read |  | ||||||
|         // stored fields from the index in the first place. |  | ||||||
|         c.setMergeable(null); |  | ||||||
|         Boolean mergeable = c.isMergeable(); |  | ||||||
|         if (mergeable == null) { |  | ||||||
|           log.error("Ephemeral error checking if change is submittable"); |  | ||||||
|           return CLICK_FAILURE_TOOLTIP; |  | ||||||
|         } |  | ||||||
|         if (!mergeable) { |  | ||||||
|           return CHANGES_NOT_MERGEABLE; |  | ||||||
|         } |  | ||||||
|         MergeOp.checkSubmitRule(c); |         MergeOp.checkSubmitRule(c); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       Boolean csIsMergeable = isPatchSetMergeable(cs); | ||||||
|  |       if (csIsMergeable == null) { | ||||||
|  |         return CLICK_FAILURE_TOOLTIP; | ||||||
|  |       } else if (!csIsMergeable) { | ||||||
|  |         return CHANGES_NOT_MERGEABLE; | ||||||
|  |       } | ||||||
|     } catch (ResourceConflictException e) { |     } catch (ResourceConflictException e) { | ||||||
|       return BLOCKED_SUBMIT_TOOLTIP; |       return BLOCKED_SUBMIT_TOOLTIP; | ||||||
|     } catch (OrmException e) { |     } catch (OrmException | IOException e) { | ||||||
|       log.error("Error checking if change is submittable", e); |       log.error("Error checking if change is submittable", e); | ||||||
|       throw new OrmRuntimeException("Could not determine problems for the change", e); |       throw new OrmRuntimeException("Could not determine problems for the change", e); | ||||||
|     } |     } | ||||||
| @@ -400,6 +407,69 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>, | |||||||
|     return change != null ? change.getStatus().name().toLowerCase() : "deleted"; |     return change != null ? change.getStatus().name().toLowerCase() : "deleted"; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public Boolean isPatchSetMergeable(ChangeSet cs) | ||||||
|  |       throws OrmException, IOException { | ||||||
|  |     Map<ChangeData, Boolean> mergeabilityMap = new HashMap<>(); | ||||||
|  |     for (ChangeData change : cs.changes()) { | ||||||
|  |       mergeabilityMap.put(change, false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Multimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch(); | ||||||
|  |     for (Branch.NameKey branch : cbb.keySet()) { | ||||||
|  |       Collection<ChangeData> targetBranch = cbb.get(branch); | ||||||
|  |       HashMap<Change.Id, RevCommit> commits = | ||||||
|  |           findCommits(targetBranch, branch.getParentKey()); | ||||||
|  |  | ||||||
|  |       Set<ObjectId> allParents = Sets.newHashSetWithExpectedSize(cs.size()); | ||||||
|  |       for (RevCommit commit : commits.values()) { | ||||||
|  |         for (RevCommit parent : commit.getParents()) { | ||||||
|  |           allParents.add(parent.getId()); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       for (ChangeData change : targetBranch) { | ||||||
|  |         RevCommit commit = commits.get(change.getId()); | ||||||
|  |         boolean isMergeCommit = commit.getParentCount() > 1; | ||||||
|  |         boolean isLastInChain = !allParents.contains(commit.getId()); | ||||||
|  |  | ||||||
|  |         // Recheck mergeability rather than using value stored in the index, | ||||||
|  |         // which may be stale. | ||||||
|  |         // TODO(dborowitz): This is ugly; consider providing a way to not read | ||||||
|  |         // stored fields from the index in the first place. | ||||||
|  |         change.setMergeable(null); | ||||||
|  |         Boolean mergeable = change.isMergeable(); | ||||||
|  |         if (mergeable == null) { | ||||||
|  |           // Skip whole check, cannot determine if mergeable | ||||||
|  |           return null; | ||||||
|  |         } | ||||||
|  |         mergeabilityMap.put(change, mergeable); | ||||||
|  |  | ||||||
|  |         if (isLastInChain && isMergeCommit && mergeable) { | ||||||
|  |           for (ChangeData c : targetBranch) { | ||||||
|  |             mergeabilityMap.put(c, true); | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return !mergeabilityMap.values().contains(Boolean.FALSE); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private HashMap<Change.Id, RevCommit> findCommits( | ||||||
|  |       Collection<ChangeData> changes, Project.NameKey project) | ||||||
|  |           throws IOException, OrmException { | ||||||
|  |     HashMap<Change.Id, RevCommit> commits = new HashMap<>(); | ||||||
|  |     try (Repository repo = repoManager.openRepository(project); | ||||||
|  |         RevWalk walk = new RevWalk(repo)) { | ||||||
|  |       for (ChangeData change : changes) { | ||||||
|  |         RevCommit commit = walk.parseCommit(ObjectId.fromString( | ||||||
|  |             psUtil.current(dbProvider.get(), change.notes()).getRevision().get())); | ||||||
|  |         commits.put(change.getId(), commit); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return commits; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private RevisionResource onBehalfOf(RevisionResource rsrc, SubmitInput in) |   private RevisionResource onBehalfOf(RevisionResource rsrc, SubmitInput in) | ||||||
|       throws AuthException, UnprocessableEntityException, OrmException { |       throws AuthException, UnprocessableEntityException, OrmException { | ||||||
|     ChangeControl caller = rsrc.getControl(); |     ChangeControl caller = rsrc.getControl(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user