| // Copyright (C) 2015 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.api.change; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.extensions.client.SubmitType.CHERRY_PICK; |
| import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY; |
| import static com.google.gerrit.extensions.client.SubmitType.MERGE_ALWAYS; |
| import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY; |
| import static com.google.gerrit.extensions.client.SubmitType.REBASE_ALWAYS; |
| import static com.google.gerrit.extensions.client.SubmitType.REBASE_IF_NECESSARY; |
| import static com.google.gerrit.testing.GerritJUnit.assertThrows; |
| |
| 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.entities.Change; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.api.projects.BranchInput; |
| import com.google.gerrit.extensions.client.SubmitType; |
| import com.google.gerrit.extensions.common.TestSubmitRuleInfo; |
| import com.google.gerrit.extensions.common.TestSubmitRuleInput; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; |
| import com.google.gerrit.server.git.meta.VersionedMetaData; |
| import com.google.gerrit.testing.ConfigSuite; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import org.eclipse.jgit.api.Git; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| @NoHttpd |
| public class SubmitTypeRuleIT extends AbstractDaemonTest { |
| @ConfigSuite.Default |
| public static Config submitWholeTopicEnabled() { |
| return submitWholeTopicEnabledConfig(); |
| } |
| |
| private class RulesPl extends VersionedMetaData { |
| private static final String FILENAME = "rules.pl"; |
| |
| private String rule; |
| |
| @Override |
| protected String getRefName() { |
| return RefNames.REFS_CONFIG; |
| } |
| |
| @Override |
| protected void onLoad() throws IOException, ConfigInvalidException { |
| rule = readUTF8(FILENAME); |
| } |
| |
| @Override |
| protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException { |
| TestSubmitRuleInput in = new TestSubmitRuleInput(); |
| in.rule = rule; |
| try { |
| gApi.changes().id(testChangeId.get()).current().testSubmitType(in); |
| } catch (RestApiException e) { |
| throw new ConfigInvalidException("Invalid submit type rule", e); |
| } |
| |
| saveUTF8(FILENAME, rule); |
| return true; |
| } |
| } |
| |
| private AtomicInteger fileCounter; |
| private Change.Id testChangeId; |
| |
| @Before |
| public void setUp() throws Exception { |
| fileCounter = new AtomicInteger(); |
| gApi.projects().name(project.get()).branch("test").create(new BranchInput()); |
| testChangeId = createChange("test", "test change").getChange().getId(); |
| } |
| |
| private void setRulesPl(String rule) throws Exception { |
| try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) { |
| RulesPl r = new RulesPl(); |
| r.load(md); |
| r.rule = rule; |
| r.commit(md); |
| } |
| projectCache.evict(project); |
| } |
| |
| private static final String SUBMIT_TYPE_FROM_SUBJECT = |
| "submit_type(fast_forward_only) :-" |
| + "gerrit:commit_message(M)," |
| + "regex_matches('.*FAST_FORWARD_ONLY.*', M)," |
| + "!.\n" |
| + "submit_type(merge_if_necessary) :-" |
| + "gerrit:commit_message(M)," |
| + "regex_matches('.*MERGE_IF_NECESSARY.*', M)," |
| + "!.\n" |
| + "submit_type(rebase_if_necessary) :-" |
| + "gerrit:commit_message(M)," |
| + "regex_matches('.*REBASE_IF_NECESSARY.*', M)," |
| + "!.\n" |
| + "submit_type(rebase_always) :-" |
| + "gerrit:commit_message(M)," |
| + "regex_matches('.*REBASE_ALWAYS.*', M)," |
| + "!.\n" |
| + "submit_type(merge_always) :-" |
| + "gerrit:commit_message(M)," |
| + "regex_matches('.*MERGE_ALWAYS.*', M)," |
| + "!.\n" |
| + "submit_type(cherry_pick) :-" |
| + "gerrit:commit_message(M)," |
| + "regex_matches('.*CHERRY_PICK.*', M)," |
| + "!.\n" |
| + "submit_type(T) :- gerrit:project_default_submit_type(T)."; |
| |
| private PushOneCommit.Result createChange(String dest, String subject) throws Exception { |
| PushOneCommit push = |
| pushFactory.create( |
| admin.newIdent(), |
| testRepo, |
| subject, |
| "file" + fileCounter.incrementAndGet(), |
| PushOneCommit.FILE_CONTENT); |
| PushOneCommit.Result r = push.to("refs/for/" + dest); |
| r.assertOkStatus(); |
| return r; |
| } |
| |
| @Test |
| public void unconditionalCherryPick() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertSubmitType(MERGE_IF_NECESSARY, r.getChangeId()); |
| setRulesPl("submit_type(cherry_pick)."); |
| assertSubmitType(CHERRY_PICK, r.getChangeId()); |
| } |
| |
| @Test |
| public void submitTypeFromSubject() throws Exception { |
| PushOneCommit.Result r1 = createChange("master", "Default 1"); |
| PushOneCommit.Result r2 = createChange("master", "FAST_FORWARD_ONLY 2"); |
| PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3"); |
| PushOneCommit.Result r4 = createChange("master", "REBASE_IF_NECESSARY 4"); |
| PushOneCommit.Result r5 = createChange("master", "REBASE_ALWAYS 5"); |
| PushOneCommit.Result r6 = createChange("master", "MERGE_ALWAYS 6"); |
| PushOneCommit.Result r7 = createChange("master", "CHERRY_PICK 7"); |
| |
| assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId()); |
| assertSubmitType(MERGE_IF_NECESSARY, r2.getChangeId()); |
| assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId()); |
| assertSubmitType(MERGE_IF_NECESSARY, r4.getChangeId()); |
| assertSubmitType(MERGE_IF_NECESSARY, r5.getChangeId()); |
| assertSubmitType(MERGE_IF_NECESSARY, r6.getChangeId()); |
| assertSubmitType(MERGE_IF_NECESSARY, r7.getChangeId()); |
| |
| setRulesPl(SUBMIT_TYPE_FROM_SUBJECT); |
| |
| assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId()); |
| assertSubmitType(FAST_FORWARD_ONLY, r2.getChangeId()); |
| assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId()); |
| assertSubmitType(REBASE_IF_NECESSARY, r4.getChangeId()); |
| assertSubmitType(REBASE_ALWAYS, r5.getChangeId()); |
| assertSubmitType(MERGE_ALWAYS, r6.getChangeId()); |
| assertSubmitType(CHERRY_PICK, r7.getChangeId()); |
| } |
| |
| @Test |
| public void submitTypeIsUsedForSubmit() throws Exception { |
| setRulesPl(SUBMIT_TYPE_FROM_SUBJECT); |
| |
| PushOneCommit.Result r = createChange("master", "CHERRY_PICK 1"); |
| |
| gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); |
| gApi.changes().id(r.getChangeId()).current().submit(); |
| |
| List<RevCommit> log = log("master", 1); |
| assertThat(log.get(0).getShortMessage()).isEqualTo("CHERRY_PICK 1"); |
| assertThat(log.get(0).name()).isNotEqualTo(r.getCommit().name()); |
| assertThat(log.get(0).getFullMessage()).contains("Change-Id: " + r.getChangeId()); |
| assertThat(log.get(0).getFullMessage()).contains("Reviewed-on: "); |
| } |
| |
| @Test |
| public void mixingSubmitTypesAcrossBranchesSucceeds() throws Exception { |
| setRulesPl(SUBMIT_TYPE_FROM_SUBJECT); |
| |
| PushOneCommit.Result r1 = createChange("master", "MERGE_IF_NECESSARY 1"); |
| |
| RevCommit initialCommit = r1.getCommit().getParent(0); |
| BranchInput bin = new BranchInput(); |
| bin.revision = initialCommit.name(); |
| gApi.projects().name(project.get()).branch("branch").create(bin); |
| |
| testRepo.reset(initialCommit); |
| PushOneCommit.Result r2 = createChange("branch", "MERGE_ALWAYS 1"); |
| |
| gApi.changes().id(r1.getChangeId()).topic(name("topic")); |
| gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve()); |
| gApi.changes().id(r2.getChangeId()).topic(name("topic")); |
| gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve()); |
| gApi.changes().id(r2.getChangeId()).current().submit(); |
| |
| assertThat(log("master", 1).get(0).name()).isEqualTo(r1.getCommit().name()); |
| |
| List<RevCommit> branchLog = log("branch", 1); |
| assertThat(branchLog.get(0).getParents()).hasLength(2); |
| assertThat(branchLog.get(0).getParent(1).name()).isEqualTo(r2.getCommit().name()); |
| } |
| |
| @Test |
| public void mixingSubmitTypesOnOneBranchFails() throws Exception { |
| setRulesPl(SUBMIT_TYPE_FROM_SUBJECT); |
| |
| PushOneCommit.Result r1 = createChange("master", "CHERRY_PICK 1"); |
| PushOneCommit.Result r2 = createChange("master", "MERGE_IF_NECESSARY 2"); |
| |
| gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve()); |
| gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve()); |
| |
| ResourceConflictException thrown = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(r2.getChangeId()).current().submit()); |
| assertThat(thrown) |
| .hasMessageThat() |
| .isEqualTo( |
| "Failed to submit 2 changes due to the following problems:\n" |
| + "Change " |
| + r1.getChange().getId() |
| + ": Change has submit type " |
| + "CHERRY_PICK, but previously chose submit type MERGE_IF_NECESSARY " |
| + "from change " |
| + r2.getChange().getId() |
| + " in the same batch"); |
| } |
| |
| @Test |
| public void invalidSubmitRuleWithNoRulesInProject() throws Exception { |
| String changeId = createChange("master", "change 1").getChangeId(); |
| |
| TestSubmitRuleInput in = new TestSubmitRuleInput(); |
| in.rule = "invalid prolog rule"; |
| // We have no rules.pl by default. The fact that the default rules are showing up here is a bug. |
| TestSubmitRuleInfo response = gApi.changes().id(changeId).current().testSubmitRule(in); |
| assertThat(response).isEqualTo(invalidPrologRuleInfo()); |
| } |
| |
| @Test |
| public void invalidSubmitRuleWithRulesInProject() throws Exception { |
| setRulesPl(SUBMIT_TYPE_FROM_SUBJECT); |
| |
| String changeId = createChange("master", "change 1").getChangeId(); |
| |
| TestSubmitRuleInput in = new TestSubmitRuleInput(); |
| in.rule = "invalid prolog rule"; |
| TestSubmitRuleInfo response = gApi.changes().id(changeId).current().testSubmitRule(in); |
| assertThat(response).isEqualTo(invalidPrologRuleInfo()); |
| } |
| |
| private static TestSubmitRuleInfo invalidPrologRuleInfo() { |
| TestSubmitRuleInfo info = new TestSubmitRuleInfo(); |
| info.status = "RULE_ERROR"; |
| info.errorMessage = "operator expected after expression at: invalid prolog rule end_of_file."; |
| return info; |
| } |
| |
| private List<RevCommit> log(String commitish, int n) throws Exception { |
| try (Repository repo = repoManager.openRepository(project); |
| Git git = new Git(repo)) { |
| ObjectId id = repo.resolve(commitish); |
| assertThat(id).isNotNull(); |
| return ImmutableList.copyOf(git.log().add(id).setMaxCount(n).call()); |
| } |
| } |
| |
| private void assertSubmitType(SubmitType expected, String id) throws Exception { |
| assertThat(gApi.changes().id(id).current().submitType()).isEqualTo(expected); |
| } |
| } |