// 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 org.junit.Assert.fail; 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.api.changes.ReviewInput; import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.client.SubmitType; 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.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.VersionedMetaData; import com.google.gerrit.testutil.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); } } 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( db, admin.getIdent(), 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()); try { gApi.changes().id(r2.getChangeId()).current().submit(); fail("Expected ResourceConflictException"); } catch (ResourceConflictException e) { assertThat(e) .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"); } } 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); } }