diff --git a/docs/README.adoc b/docs/README.adoc index 5b7566ea5..5d6a9ab1f 100644 --- a/docs/README.adoc +++ b/docs/README.adoc @@ -359,6 +359,62 @@ node { Note though that with this approach the changelog will not show correctly. +== Declarative Pipeline Jobs + +You can configure the pipeline checkout in the job configuration to use the +"Gerrit Trigger with merge commit support" choosing strategy. This strategy +behaves similar to the original "Gerrit Trigger" strategy with the exception +that for change-merged events where Gerrit automatically creates a merge commit, +it will select the revision for the merge commit rather than the revision for +the patchset that was submitted. This ensures that the Jenkinsfile is checked +out correctly in the event the Gerrit resolved a merge for that file. See +https://issues.jenkins.io/browse/JENKINS-65481 for all the details. + +Configure your job as normal for patchset-created and change-merged event triggers then use +the following settings in the Pipeline section of your job configuration +(you can use a similar setup for freestyle jobs): + +Definition: Pipeline script from SCM + - SCM: Git + - Repositories + - Set your repository URL and credentials + - Name: origin + - Click the "Advanced" button + - Refspec: $GERRIT_REFSPEC +refs/heads/$GERRIT_BRANCH:refs/remotes/origin/$GERRIT_BRANCH + - Branches to build + - Branch specifier: refs/heads/$GERRIT_BRANCH + - Additional Behaviours + - Strategy for choosing what to build: Gerrit Trigger with merge commit support + +then in your Jenkinsfile pipeline code rely on the automatic checkout or disable the automatic checkout and use +"scm checkout" as shown below: + +[source,syntaxhighlighter-pre] +---- +pipeline { + options { + skipDefaultCheckout true + // ... + } + stages { + stage('checkout') { + checkout scm + } + //... + } +} +---- + +This method of skipping the default checkout is useful if you want to checkout into a subdirectory of your +workspace by wrapping the checkout in a dir directive, for example: + +[source,syntaxhighlighter-pre] +---- +dir("${env.WORKSPACE}/scm") { + checkout scm +} +---- + == Tips & Tricks This section contains some useful tips and tricks that users has come up diff --git a/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerBuildChooserWithMergeCommitSupport.java b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerBuildChooserWithMergeCommitSupport.java new file mode 100644 index 000000000..a06d38f5b --- /dev/null +++ b/src/main/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerBuildChooserWithMergeCommitSupport.java @@ -0,0 +1,275 @@ +/* + * The MIT License + * + * Copyright 2010 Andrew Bayer. All rights reserved. + * Copyright 2013 Sony Mobile Communications AB. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger; + + +import com.sonymobile.tools.gerrit.gerritevents.dto.events.ChangeBasedEvent; +import com.sonymobile.tools.gerrit.gerritevents.dto.events.ChangeMerged; +import com.sonymobile.tools.gerrit.gerritevents.dto.events.GerritTriggeredEvent; +import com.sonymobile.tools.gerrit.gerritevents.dto.events.RefUpdated; + +import hudson.Extension; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.model.Result; +import hudson.plugins.git.GitException; +import hudson.plugins.git.Revision; +import hudson.plugins.git.Branch; +import hudson.plugins.git.util.Build; +import hudson.plugins.git.util.BuildChooser; +import hudson.plugins.git.util.BuildChooserContext; +import hudson.plugins.git.util.BuildChooserDescriptor; +import hudson.plugins.git.util.BuildData; +import hudson.remoting.VirtualChannel; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.gitclient.RepositoryCallback; +import org.kohsuke.stapler.DataBoundConstructor; +import org.eclipse.jgit.lib.ObjectId; + + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.logging.Logger; + +import com.sonyericsson.hudson.plugins.gerrit.trigger.Messages; + +/** + * Used by the git plugin to determine the revision to build. This forks off + * the original build chooser to add support for handling automatic merge commits. + * It was necessary to add an additional chooser as the change could not be made + * in a backward compatible way. + */ +public class GerritTriggerBuildChooserWithMergeCommitSupport extends BuildChooser { + private static final long serialVersionUID = 2003462680723330645L; + + /** + * Used by XStream for something. + */ + @SuppressWarnings("unused") + private final String separator = "#"; + + /** + * Default constructor. + */ + @DataBoundConstructor + public GerritTriggerBuildChooserWithMergeCommitSupport() { + } + + //CS IGNORE RedundantThrows FOR NEXT 30 LINES. REASON: Informative, and could happen. + + /** + * Determines which Revisions to build. + * + * Doesn't care about branches. + * + * @param isPollCall whether this is being called from Git polling + * @param singleBranch The branch + * @param git The GitClient API object + * @param listener TaskListener for logging, etc + * @param data the historical BuildData object + * @param context the remote context + * @return A Collection containing the new revision. + * + * @throws GitException in case of error + * @throws IOException In case of error + * @throws InterruptedException In case of error + */ + @Override + public Collection getCandidateRevisions(boolean isPollCall, String singleBranch, + GitClient git, TaskListener listener, + BuildData data, BuildChooserContext context) + throws GitException, IOException, InterruptedException { + + try { + String rev = context.actOnBuild(new GetGerritEventRevision()); + if (rev == null) { + rev = "FETCH_HEAD"; + } + + String refspec = context.actOnBuild(new GetGerritEventRefspec()); + if (refspec == null) { + refspec = singleBranch; + } + + ObjectId sha1 = git.revParse(rev); + + Revision revision = new Revision(sha1); + revision.getBranches().add(new Branch(refspec, sha1)); + + return Collections.singletonList(revision); + } catch (GitException e) { + // branch does not exist, there is nothing to build + return Collections.emptyList(); + } + } + + @Override + public Build prevBuildForChangelog(String singleBranch, BuildData data, GitClient git, + BuildChooserContext context) throws InterruptedException, IOException { + if (data != null) { + ObjectId sha1 = git.revParse("FETCH_HEAD"); + + // Now we cheat and add the parent as the last build on the branch, so we can + // get the changelog working properly-ish. + ObjectId parentSha1 = getFirstParent(sha1, git); + Revision parentRev = new Revision(parentSha1); + parentRev.getBranches().add(new Branch(singleBranch, parentSha1)); + + int prevBuildNum = 0; + Result r = null; + + Build lastBuild = data.getLastBuildOfBranch(singleBranch); + if (lastBuild != null) { + prevBuildNum = lastBuild.getBuildNumber(); + r = lastBuild.getBuildResult(); + } + + return new Build(parentRev, prevBuildNum, r); + } else { + //Hmm no sure what to do here, but the git plugin can handle us returning null here + return null; + } + } + + //CS IGNORE RedundantThrows FOR NEXT 30 LINES. REASON: Informative, and could happen. + /** + * Gets the top parent of the given revision. + * + * @param id Revision + * @param git GitClient API object + * @return object id of Revision's parent, or of Revision itself if there is no parent + * @throws GitException In case of error in git call + * @throws InterruptedException if the repository handling gets interrupted + * @throws IOException in case of communication errors. + */ + @SuppressWarnings("serial") + private ObjectId getFirstParent(final ObjectId id, GitClient git) + throws GitException, IOException, InterruptedException { + return git.withRepository(new RepositoryCallback() { + @Override + public ObjectId invoke(Repository repository, VirtualChannel virtualChannel) + throws IOException, InterruptedException { + ObjectId result = null; + try (RevWalk walk = new RevWalk(repository)) { + RevCommit commit = walk.parseCommit(id); + if (commit.getParentCount() > 0) { + result = commit.getParent(0); + } else { + // If this is the first commit in the git, there is no parent. + result = id; + } + } catch (Exception e) { + throw new GitException("Failed to find parent id. ", e); + } + return result; + } + }); + } + + /** + * Descriptor for GerritTriggerBuildChooserWithMergeCommitSupport. + */ + @Extension(optional = true) + public static final class DescriptorImpl extends BuildChooserDescriptor { + @Override + public String getDisplayName() { + return Messages.GerritTriggerBuildChooserWithMergeCommitSupport_DisplayName(); + } + + @Override + public String getLegacyId() { + return Messages.GerritTriggerBuildChooserWithMergeCommitSupport_DisplayName(); + } + } + + /** + * Retrieve the Gerrit event revision + */ + private static class GetGerritEventRevision + implements BuildChooserContext.ContextCallable, String> { + static final long serialVersionUID = 0L; + @Override + public String invoke(Run build, VirtualChannel channel) { + GerritCause cause = build.getCause(GerritCause.class); + if (cause != null) { + GerritTriggeredEvent event = cause.getEvent(); + if (event instanceof ChangeMerged) { + // when gerrit creates a merge commit for some submit types, we need the newrev or we get the + // changes from the patchset before the merge and build the wrong source, in the case that + // there is no merge commit, the newrev will match the patchset revision + String newRev = ((ChangeMerged)event).getNewRev(); + if (newRev != null) { + return newRev; + } + // else we are on an old version of gerrit that is not reporting newrev + // so fall through to the old behavior + } + if (event instanceof ChangeBasedEvent) { + return ((ChangeBasedEvent)event).getPatchSet().getRevision(); + } + if (event instanceof RefUpdated) { + return ((RefUpdated)event).getRefUpdate().getNewRev(); + } + } + return null; + } + } + + /** + * Retrieve the Gerrit refspec + */ + private static class GetGerritEventRefspec + implements BuildChooserContext.ContextCallable, String> { + static final long serialVersionUID = 0L; + @Override + public String invoke(Run build, VirtualChannel channel) { + GerritCause cause = build.getCause(GerritCause.class); + if (cause != null) { + GerritTriggeredEvent event = cause.getEvent(); + // For change-merged we need the project refname which is the ref for the branch we merged onto in + // case Gerrit created an automatic merge commit. The project object from the event stream is not + // available in the DTO objects so use the next best thing which is the branch name from the Change. + if (event instanceof ChangeMerged) { + return "refs/heads/" + ((ChangeMerged)event).getChange().getBranch(); + } + if (event instanceof ChangeBasedEvent) { + return ((ChangeBasedEvent)event).getPatchSet().getRef(); + } + if (event instanceof RefUpdated) { + return ((RefUpdated)event).getRefUpdate().getRefName(); + } + } + return null; + } + } + + private static final Logger LOGGER = Logger.getLogger(GerritTriggerBuildChooser.class.getName()); +} diff --git a/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/Messages.properties b/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/Messages.properties index 1e28b7049..6c4e852f9 100644 --- a/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/Messages.properties +++ b/src/main/resources/com/sonyericsson/hudson/plugins/gerrit/trigger/Messages.properties @@ -204,3 +204,4 @@ GerritProjectListUpdater.For=GerritProjectListUpdater for server: {0} GerritMissedEventsPlaybackManager.For=GerritMissedEventsPlaybackManager for server: {0} NotANumber=Not a number NoSuchJobExists=No such job \u2018{0}\u2019 exists. Perhaps you meant \u2018{1}\u2019? +GerritTriggerBuildChooserWithMergeCommitSupport.DisplayName=Gerrit Trigger with merge commit support diff --git a/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerBuildChooserStorageTest.java b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerBuildChooserStorageTest.java new file mode 100644 index 000000000..ec304b230 --- /dev/null +++ b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerBuildChooserStorageTest.java @@ -0,0 +1,168 @@ +package com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.htmlunit.html.HtmlButton; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlOption; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.html.HtmlRadioButtonInput; +import org.htmlunit.html.HtmlSelect; + +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.RestartableJenkinsRule; + +import hudson.model.FreeStyleProject; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.impl.BuildChooserSetting; +import hudson.plugins.git.util.BuildChooser; +import hudson.scm.SCM; + +/** + * Tests for configuration and storage of {@link GerritTriggerBuildChooser} and + * {@link GerritTriggerBuildChooserWithMergeCommitSupport}. + * + * @author Eric Isakson + */ +public class GerritTriggerBuildChooserStorageTest { + + /** + * XPath selector to identify the Git radio button in the SCM config. + */ + private static final String GIT_RADIO_BUTTON_XPATH = + "//input[@type = 'radio' and @name = 'scm' and normalize-space(..) = 'Git']"; + + /** + * XPath selector to identify the Git add additional behaviours button in the page. + */ + private static final String GIT_ADD_ADDITIONAL_BEHAVIORS_BUTTON_XPATH = "//button[@suffix='extensions']"; + + /** + * XPath selector to identify the Git choosing strategy menu item after the add button is clicked. + */ + private static final String GIT_CHOOSING_STRATEGY_MENUITEM_XPATH = + "//button[normalize-space(text())='Strategy for choosing what to build']"; + + /** + * XPath selector to identify the select option list for the choosing strategy in the page. + */ + private static final String GIT_CHOOSING_STRATEGY_SELECT_XPATH = + "//self::node()[@descriptorid='" + BuildChooserSetting.class.getCanonicalName() + "']//select"; + + /** + * Max number of times to loop while waiting on page update. + */ + private static final int MAX_RETRIES_WAITING_FOR_PAGE_UPDATE = 10; + + /** + * Time to sleep between condition checks while waiting on page update. + */ + private static final long SLEEP_MILLIS_WAITING_FOR_PAGE_UPDATE = 1000L; + + /** + * Jenkins rule. + */ + // CS IGNORE VisibilityModifier FOR NEXT 2 LINES. REASON: JenkinsRule. + @Rule + public RestartableJenkinsRule rr = new RestartableJenkinsRule(); + + /** + * Verify the project has the expected settings for the given chooser. + * + * @param p The project to verify. + * @param expected The expected chooser. + */ + private void assertBuildChooser(FreeStyleProject p, BuildChooser expected) { + assertNotNull(p); + assertNotNull(expected); + SCM scm = p.getScm(); + assertTrue(scm instanceof GitSCM); + GitSCM gitSCM = (GitSCM)scm; + List buildChooserSettings = gitSCM.getExtensions().getAll(BuildChooserSetting.class); + assertEquals(1, buildChooserSettings.size()); + assertEquals(expected.getClass(), buildChooserSettings.get(0).getBuildChooser().getClass()); + } + + /** + * Confirm configuration state is restored properly after reload and restart. + * + * @param chooser The chooser to test. + * @throws Exception If anything unexpected happens. + */ + private void testStorageForChooser(BuildChooser chooser) throws Exception { + rr.then(r -> { + FreeStyleProject p = r.createFreeStyleProject("testproject"); + + // Setup the chooser using the web page form + HtmlPage page = r.createWebClient().getPage(p, "configure"); + HtmlRadioButtonInput scmGit = (HtmlRadioButtonInput)page.getFirstByXPath(GIT_RADIO_BUTTON_XPATH); + scmGit.setChecked(true); + page = scmGit.click(); + assertNotNull(page); + HtmlButton addAdditionalBehaviorButton = + (HtmlButton)page.getFirstByXPath(GIT_ADD_ADDITIONAL_BEHAVIORS_BUTTON_XPATH); + assertNotNull(addAdditionalBehaviorButton); + page = addAdditionalBehaviorButton.click(); + assertNotNull(page); + HtmlButton gitChoosingStrategyLink = (HtmlButton)page.getFirstByXPath(GIT_CHOOSING_STRATEGY_MENUITEM_XPATH); + assertNotNull(gitChoosingStrategyLink); + page = gitChoosingStrategyLink.click(); + assertNotNull(page); + // Retry with wait in between while the page updates and fail if we do not find the select + // element within a fixed number of retries... + // CS IGNORE LineLength FOR NEXT 1 LINES. REASON: Long URL. + // See https://stackoverflow.com/questions/17843521/get-the-changed-html-content-after-its-updated-by-javascript-htmlunit + int amountOfTries = MAX_RETRIES_WAITING_FOR_PAGE_UPDATE; + HtmlSelect select = (HtmlSelect)page.getFirstByXPath(GIT_CHOOSING_STRATEGY_SELECT_XPATH); + while (amountOfTries > 0 && select == null) { + amountOfTries--; + synchronized (page) { + page.wait(SLEEP_MILLIS_WAITING_FOR_PAGE_UPDATE); + } + select = (HtmlSelect)page.getFirstByXPath(GIT_CHOOSING_STRATEGY_SELECT_XPATH); + } + assertNotNull(select); + HtmlOption option = select.getOptionByText(chooser.getDisplayName()); + page = select.setSelectedAttribute(option, true); + page = option.click(); + HtmlForm form = page.getFormByName("config"); + r.submit(form, "Submit"); // This is the "Save" button + + // Round trip the configuration + p = r.configRoundtrip(p); + + // Confirm the chooser is set correctly after the config round trip + assertBuildChooser(p, chooser); + }); + rr.then(r -> { + // Confirm the chooser is still set correctly after the restart + FreeStyleProject p = r.jenkins.getItemByFullName("testproject", FreeStyleProject.class); + assertBuildChooser(p, chooser); + }); + } + + /** + * Test for {@link GerritTriggerBuildChooser}. + * + * @throws Exception If anything unexpected happens. + */ + @Test + public void testStorageForGerritTriggerBuildChooser() throws Exception { + testStorageForChooser(new GerritTriggerBuildChooser()); + } + + /** + * Test for {@link GerritTriggerBuildChooserWithMergeCommitSupport}. + * + * @throws Exception If anything unexpected happens. + */ + @Test + public void testStorageForGerritTriggerBuildChooserWithMergeCommitSupport() throws Exception { + testStorageForChooser(new GerritTriggerBuildChooserWithMergeCommitSupport()); + } +} diff --git a/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerBuildChooserWithMergeCommitSupportTest.java b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerBuildChooserWithMergeCommitSupportTest.java new file mode 100644 index 000000000..240137fa1 --- /dev/null +++ b/src/test/java/com/sonyericsson/hudson/plugins/gerrit/trigger/hudsontrigger/GerritTriggerBuildChooserWithMergeCommitSupportTest.java @@ -0,0 +1,198 @@ +package com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger; + +import hudson.plugins.git.util.BuildChooserContext; +import hudson.plugins.git.Revision; +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import java.util.Collection; +import hudson.model.Job; +import hudson.model.Run; +import hudson.model.FreeStyleProject; +import hudson.model.FreeStyleBuild; +import hudson.EnvVars; +import java.io.IOException; +import java.io.Serializable; +import hudson.model.Hudson; +import org.jenkinsci.plugins.gitclient.GitClient; + +import com.sonymobile.tools.gerrit.gerritevents.dto.events.ChangeMerged; +import com.sonymobile.tools.gerrit.gerritevents.dto.events.PatchsetCreated; +import com.sonyericsson.hudson.plugins.gerrit.trigger.mock.Setup; +import org.eclipse.jgit.lib.ObjectId; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link GerritTriggerBuildChooserWithMergeCommitSupport}. + * @author Eric Isakson + */ +public class GerritTriggerBuildChooserWithMergeCommitSupportTest { + /** + * Copied from the Git plugin because the real implementation is private. + * + * Ideally we can find a way to have this work somehow without needing to have this copy... + */ + static class BuildChooserContextImpl implements BuildChooserContext, Serializable { + private static final long serialVersionUID = 1L; + + final Job project; + final Run build; + final EnvVars environment; + + /** + * Provides context for running closures while determining what to build + * @param project the Jenkins project + * @param build the Jenkins build + * @param environment the environment + */ + BuildChooserContextImpl(Job project, Run build, EnvVars environment) { + this.project = project; + this.build = build; + this.environment = environment; + } + + /** + * Perform some closure, executing on the build + * @param The return type from the closure + * @param callable the closure to run + * @throws IOException if IO cannot be performed + * @throws InterruptedException if the process is interrupted + * @return closure return value + */ + public T actOnBuild(ContextCallable, T> callable) + throws IOException, InterruptedException { + return callable.invoke(build, Hudson.MasterComputer.localChannel); + } + + /** + * Perform some closure, executing on the project + * @param The return type from the closure + * @param callable the closure to run + * @throws IOException if IO cannot be performed + * @throws InterruptedException if the process is interrupted + * @return closure return value + */ + public T actOnProject(ContextCallable, T> callable) + throws IOException, InterruptedException { + return callable.invoke(project, Hudson.MasterComputer.localChannel); + } + + /** + * Get the build + * @return Jenkins build + */ + public Run getBuild() { + return build; + } + + /** + * Get the project + * @return environment variables + */ + public EnvVars getEnvironment() { + return environment; + } + } + + /** + * Tests {@link GerritTriggerBuildChooser} if it correctly determines revision. + * + * @throws Exception if so. + */ + @Test + public void testGerritTriggerBuildChooser() throws Exception { + GerritTriggerBuildChooserWithMergeCommitSupport chooser = new GerritTriggerBuildChooserWithMergeCommitSupport(); + final ObjectId fetchHead = ObjectId.fromString("7f3547c6d55946e25e99a847b5160d69e59994ba"); + final ObjectId patchsetRevision = ObjectId.fromString("38b0940738376ee1b66c332a2cb6d4d37bafa4e4"); + // newRev here does not match patchsetRevision to simulate gerrit automatically creating a merge commit + final ObjectId newRev = ObjectId.fromString("14332a762fa01cfcb65e637ecce3bc621b44a381"); + final String singleBranch = "origin/master"; + final String changeBranch = "master"; + final String patchsetRefspec = "refs/changes/98/99498/2"; + final String changeMergedRefspec = "refs/heads/master"; + + // Mock the necessary objects we will need to make this work + FreeStyleProject p = mock(FreeStyleProject.class); + FreeStyleBuild b = mock(FreeStyleBuild.class); + GitClient git = mock(GitClient.class); + when(git.revParse("FETCH_HEAD")).thenReturn(fetchHead); + + BuildChooserContextImpl context = new BuildChooserContextImpl(p, b, null); + + // get the candidate revision(s) + Collection revs = chooser.getCandidateRevisions(true, singleBranch, git, null, null, context); + + // Check that we correctly use branch when no gerrit revision is used + assertEquals(1, revs.size()); + assertEquals(1, revs.iterator().next().getBranches().size()); + assertEquals(singleBranch, revs.iterator().next().getBranches().iterator().next().getName()); + assertEquals(fetchHead, revs.iterator().next().getBranches().iterator().next().getSHA1()); + + // Mock the objects to report a gerrit revision was built + // build.getCause returns some object which reports the event correctly + PatchsetCreated patchsetCreated = Setup.createPatchsetCreated(); + patchsetCreated.getPatchSet().setRef(patchsetRefspec); + patchsetCreated.getPatchSet().setRevision(patchsetRevision.toString()); + + GerritCause gerritCause = new GerritCause(); + gerritCause.setEvent(patchsetCreated); + when(b.getCause(GerritCause.class)).thenReturn(gerritCause); + when(git.revParse(patchsetRefspec)).thenReturn(patchsetRevision); + when(git.revParse(patchsetRevision.toString())).thenReturn(patchsetRevision); + + // get the candidate revision(s) + revs = chooser.getCandidateRevisions(true, singleBranch, git, null, null, context); + + // Check that we correctly use branch when a gerrit revision is used + assertEquals(1, revs.size()); + assertEquals(1, revs.iterator().next().getBranches().size()); + assertEquals(patchsetRefspec, revs.iterator().next().getBranches().iterator().next().getName()); + assertEquals(patchsetRevision, revs.iterator().next().getBranches().iterator().next().getSHA1()); + + // Mock the objects to report a gerrit revision was built + // build.getCause returns a change-merged event without a newrev to test old gerrit versions + ChangeMerged changeMerged = Setup.createChangeMerged(); + changeMerged.getPatchSet().setRef(patchsetRefspec); + changeMerged.getPatchSet().setRevision(patchsetRevision.toString()); + changeMerged.getChange().setBranch(changeBranch); + + gerritCause = new GerritCause(); + gerritCause.setEvent(changeMerged); + when(b.getCause(GerritCause.class)).thenReturn(gerritCause); + when(git.revParse(changeMergedRefspec)).thenReturn(patchsetRevision); + when(git.revParse(patchsetRevision.toString())).thenReturn(patchsetRevision); + + // get the candidate revision(s) + revs = chooser.getCandidateRevisions(true, singleBranch, git, null, null, context); + + // Check that we correctly use branch when a gerrit revision is used + assertEquals(1, revs.size()); + assertEquals(1, revs.iterator().next().getBranches().size()); + assertEquals(changeMergedRefspec, revs.iterator().next().getBranches().iterator().next().getName()); + assertEquals(patchsetRevision, revs.iterator().next().getBranches().iterator().next().getSHA1()); + + // Mock the objects to report a gerrit revision was built + // build.getCause returns a change-merged event with a newrev value to test current gerrit versions + changeMerged = Setup.createChangeMerged(); + changeMerged.getPatchSet().setRef(patchsetRefspec); + changeMerged.getPatchSet().setRevision(patchsetRevision.toString()); + changeMerged.getChange().setBranch(changeBranch); + changeMerged.setNewRev(newRev.toString()); + + gerritCause = new GerritCause(); + gerritCause.setEvent(changeMerged); + when(b.getCause(GerritCause.class)).thenReturn(gerritCause); + when(git.revParse(changeMergedRefspec)).thenReturn(newRev); + when(git.revParse(newRev.toString())).thenReturn(newRev); + + // get the candidate revision(s) + revs = chooser.getCandidateRevisions(true, singleBranch, git, null, null, context); + + // Check that we correctly use branch when a gerrit revision is used + assertEquals(1, revs.size()); + assertEquals(1, revs.iterator().next().getBranches().size()); + assertEquals(changeMergedRefspec, revs.iterator().next().getBranches().iterator().next().getName()); + assertEquals(newRev, revs.iterator().next().getBranches().iterator().next().getSHA1()); + } +}