/* * The MIT License * * Copyright (c) 2016 CloudBees, Inc. * * 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 jenkins.scm.impl.mock; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.Items; import hudson.model.Job; import hudson.model.Run; import hudson.model.TaskListener; import hudson.scm.ChangeLogParser; import hudson.scm.ChangeLogSet; import hudson.scm.PollingResult; import hudson.scm.RepositoryBrowser; import hudson.scm.SCMDescriptor; import hudson.scm.SCMRevisionState; import hudson.util.ListBoxModel; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.Serializable; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import jenkins.scm.api.SCM2; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadOrigin; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import org.apache.commons.io.IOUtils; import org.codehaus.plexus.util.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.xml.sax.SAXException; public class MockSCM extends SCM2 implements Serializable { private final String controllerId; private final String repository; private final SCMHead head; private final SCMRevision revision; private transient MockSCMController controller; @DataBoundConstructor public MockSCM(String controllerId, String repository, String head, String revision) { this.controllerId = controllerId; this.repository = repository; // implementations of SCM API need not use the same convention for SCMHead.getName() as their underlying // SCM does, e.g. GitHub calls pull requests PR-# or PR-#-head or PR-#-merge // we simulate this here in order to ensure that the branch api has not made any implicit assumptions // though it would be simpler to not have a unified namespace for tags and branches in MockSCMController // and to have MockSCMController use a separate set of API methods in checking out change requests // instead of merging them into the MockSCMController namespace under change-request/# Matcher m = Pattern.compile("CR-(\\d+)(-[a-zA-Z]+)?").matcher(head); if (m.matches()) { int number = Integer.parseInt(m.group(1)); String target = null; String targetRevision = null; Set<MockChangeRequestFlags> flags = null; try { target = controller().getTarget(repository, number); targetRevision = controller().getRevision(repository, target); flags = controller().getFlags(repository, number); } catch (IOException e) { // ignore } ChangeRequestCheckoutStrategy strategy = ChangeRequestCheckoutStrategy.HEAD; String strategyStr = m.group(2); if (StringUtils.isNotBlank(strategyStr)) { for (ChangeRequestCheckoutStrategy s : ChangeRequestCheckoutStrategy.values()) { if (strategyStr.equals("-"+(s.name().toLowerCase(Locale.ENGLISH)))) { strategy = s; break; } } } MockChangeRequestSCMHead h = new MockChangeRequestSCMHead( flags == null || !flags.contains(MockChangeRequestFlags.FORK) ? SCMHeadOrigin.DEFAULT : new SCMHeadOrigin.Fork("fork"), number, target, strategy, StringUtils.isNotBlank(strategyStr) ); this.head = h; this.revision = new MockChangeRequestSCMRevision(h, new MockSCMRevision(h.getTarget(), targetRevision), revision); } else if (head.startsWith("TAG:")) { long timestamp = 0; try { timestamp = controller().getTagTimestamp(repository, head.substring(4)); } catch (IOException e) { // ignore } this.head = new MockTagSCMHead(head.substring(4), timestamp); this.revision = revision == null ? null : new MockSCMRevision(this.head, revision); } else { this.head = new MockSCMHead(head); this.revision = revision == null ? null : new MockSCMRevision(this.head, revision); } } public MockSCM(MockSCMSource config, SCMHead head, SCMRevision revision) { this.controllerId = config.getControllerId(); this.repository = config.getRepository(); this.head = head; this.revision = revision; } public MockSCM(MockSCMController controller, String repository, SCMHead head, SCMRevision revision) { this.controllerId = controller.getId(); this.controller = controller; this.repository = repository; this.revision = revision; this.head = head; } public String getControllerId() { return controllerId; } private MockSCMController controller() { if (controller == null) { controller = MockSCMController.lookup(controllerId); } return controller; } public String getRepository() { return repository; } public String getHead() { if (head instanceof MockChangeRequestSCMHead) { return "CR-" + ((MockChangeRequestSCMHead) head).getNumber(); } if (head instanceof MockTagSCMHead) { return "TAG:" + head.getName(); } return head.getName(); } public String getRevision() { if (revision instanceof MockSCMRevision) { return ((MockSCMRevision) revision).getHash(); } if (revision instanceof MockChangeRequestSCMRevision) { return ((MockChangeRequestSCMRevision) revision).getHash(); } return null; } @Override public boolean supportsPolling() { return true; } @Override public boolean requiresWorkspaceForPolling() { return false; } @Override public PollingResult compareRemoteRevisionWith(@Nonnull Job<?, ?> project, @Nullable Launcher launcher, @Nullable FilePath workspace, @Nonnull TaskListener listener, @Nonnull SCMRevisionState baseline) throws IOException, InterruptedException { if (baseline instanceof MockSCMRevisionState) { String revision; if (this.revision instanceof MockSCMRevision) { revision = ((MockSCMRevision) this.revision).getHash(); } else if (this.revision instanceof MockChangeRequestSCMRevision) { revision = ((MockChangeRequestSCMRevision) this.revision).getHash(); } else { if (head instanceof MockChangeRequestSCMHead) { revision = controller() .getRevision(repository, "change-request/" + ((MockChangeRequestSCMHead) head).getNumber()); } else { revision = controller().getRevision(repository, head.getName()); } } if (((MockSCMRevisionState) baseline).getRevision().getHash().equals(revision)) { return PollingResult.NO_CHANGES; } return PollingResult.SIGNIFICANT; } return PollingResult.BUILD_NOW; } @Override public void checkout(@Nonnull Run<?, ?> build, @Nonnull Launcher launcher, @Nonnull FilePath workspace, @Nonnull TaskListener listener, @CheckForNull File changelogFile, @CheckForNull SCMRevisionState baseline) throws IOException, InterruptedException { String identifier; if (this.revision instanceof MockSCMRevision) { identifier = ((MockSCMRevision) this.revision).getHash(); } else if (this.revision instanceof MockChangeRequestSCMRevision) { identifier = ((MockChangeRequestSCMRevision) this.revision).getHash(); } else { identifier = head.getName(); } String hash = controller().checkout(workspace, repository, identifier); FileWriter writer = new FileWriter(changelogFile); try { Items.XSTREAM2.toXML(controller().log(repository, hash), writer); } finally { IOUtils.closeQuietly(writer); } build.addAction(new MockSCMRevisionState(new MockSCMRevision(head, hash))); } @Override public SCMRevisionState calcRevisionsFromBuild(@Nonnull Run<?, ?> build, @Nullable FilePath workspace, @Nullable Launcher launcher, @Nonnull TaskListener listener) throws IOException, InterruptedException { return build.getAction(MockSCMRevisionState.class); } @Override public ChangeLogParser createChangeLogParser() { return new ChangeLogParser() { @Override public ChangeLogSet<? extends ChangeLogSet.Entry> parse(Run build, RepositoryBrowser<?> browser, File changelogFile) throws IOException, SAXException { List<MockSCMController.LogEntry> entries = (List<MockSCMController.LogEntry>) Items.XSTREAM2.fromXML(changelogFile); return new MockSCMChangeLogSet(build, browser, entries); } }; } @Override public String toString() { return "MockSCM{" + "controllerId='" + controllerId + '\'' + ", repository='" + repository + '\'' + ", head=" + head + ", revision=" + revision + '}'; } public static class MockSCMRevisionState extends SCMRevisionState { private final MockSCMRevision revision; public MockSCMRevisionState(MockSCMRevision revision) { this.revision = revision; } public MockSCMRevision getRevision() { return revision; } } @Extension public static class DescriptorImpl extends SCMDescriptor<MockSCM> { public DescriptorImpl() { super(MockSCMRepositoryBrowser.class); } @Override public String getDisplayName() { return "Mock SCM"; } public ListBoxModel doFillControllerIdItems() { ListBoxModel result = new ListBoxModel(); for (MockSCMController c : MockSCMController.all()) { result.add(c.getId()); } return result; } public ListBoxModel doFillRepositoryItems(@QueryParameter String controllerId) throws IOException { ListBoxModel result = new ListBoxModel(); MockSCMController c = MockSCMController.lookup(controllerId); if (c != null) { for (String r : c.listRepositories()) { result.add(r); } } return result; } public ListBoxModel doFillHeadItems(@QueryParameter String controllerId, @QueryParameter String repository) throws IOException { ListBoxModel result = new ListBoxModel(); MockSCMController c = MockSCMController.lookup(controllerId); if (c != null) { for (String r : c.listBranches(repository)) { result.add(r); } for (String r : c.listTags(repository)) { result.add("TAG:" + r); } for (Integer r : c.listChangeRequests(repository)) { result.add("CR-" + r); } } return result; } } }