/******************************************************************************* * * Copyright (c) 2004-2009 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Stephen Connolly, InfraDNA, Inc. * * *******************************************************************************/ package hudson.scm; import hudson.ExtensionPoint; import hudson.FilePath; import hudson.Launcher; import hudson.DescriptorExtensionList; import hudson.Extension; import hudson.Util; import hudson.security.PermissionGroup; import hudson.security.Permission; import hudson.tasks.Builder; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Describable; import hudson.model.TaskListener; import hudson.model.Node; import hudson.model.WorkspaceCleanupThread; import hudson.model.Hudson; import hudson.model.Descriptor; import hudson.model.Api; import hudson.model.Action; import hudson.model.AbstractProject.AbstractProjectDescriptor; import hudson.util.IOUtils; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.Map; import java.util.List; import java.util.ArrayList; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; /** * Captures the configuration information in it. * * <p> To register a custom {@link SCM} implementation from a plugin, put * {@link Extension} on your {@link SCMDescriptor}. * * <p> Use the "project-changes" view to render change list to be displayed at * the project level. The default implementation simply aggregates change lists * from builds, but your SCM can provide different views. The view gets the * "builds" variable which is a list of builds that are selected for the * display. * * <p> If you are interested in writing a subclass in a plugin, also take a look * at <a href="http://wiki.hudson-ci.org/display/HUDSON/Writing+an+SCM+plugin"> * "Writing an SCM plugin"</a> wiki article. * * @author Kohsuke Kawaguchi */ @ExportedBean public abstract class SCM implements Describable<SCM>, ExtensionPoint { /** * Stores {@link AutoBrowserHolder}. Lazily created. */ private transient AutoBrowserHolder autoBrowserHolder; /** * Expose {@link SCM} to the remote API. */ public Api getApi() { return new Api(this); } /** * Returns the {@link RepositoryBrowser} for files controlled by this * {@link SCM}. * * @return null to indicate that there's no explicitly configured browser * for this SCM instance. * * @see #getEffectiveBrowser() */ public RepositoryBrowser<?> getBrowser() { return null; } /** * Type of this SCM. * * Exposed so that the client of the remote API can tell what SCM this is. */ @Exported public String getType() { return getClass().getName(); } /** * Returns the applicable {@link RepositoryBrowser} for files controlled by * this {@link SCM}. * * <p> This method attempts to find applicable browser from other job * configurations. */ @Exported(name = "browser") public final RepositoryBrowser<?> getEffectiveBrowser() { RepositoryBrowser<?> b = getBrowser(); if (b != null) { return b; } if (autoBrowserHolder == null) { autoBrowserHolder = new AutoBrowserHolder(this); } return autoBrowserHolder.get(); } /** * Returns true if this SCM supports * {@link #poll(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState) poling}. * * @since 1.105 */ public boolean supportsPolling() { return true; } /** * Returns true if this SCM requires a checked out workspace for doing * polling. * * <p> This flag affects the behavior of Hudson when a job lost its * workspace (typically due to a slave outage.) If this method returns false * and polling is configured, then that would immediately trigger a new * build. * * <p> This flag also affects the mutual exclusion control between builds * and polling. If this methods returns false, polling will continu * asynchronously even when a build is in progress, but otherwise the * polling activity is blocked if a build is currently using a workspace. * * <p> The default implementation returns true. * * <p> See issue #1348 for more discussion of this feature. * * @since 1.196 */ public boolean requiresWorkspaceForPolling() { return true; } /** * Called before a workspace is deleted on the given node, to provide SCM an * opportunity to perform clean up. * * <p> Hudson periodically scans through all the slaves and removes old * workspaces that are deemed unnecesasry. This behavior is implemented in * {@link WorkspaceCleanupThread}, and it is necessary to control the disk * consumption on slaves. If we don't do this, in a long run, all the slaves * will have workspaces for all the projects, which will be prohibitive in * big Hudson. * * <p> However, some SCM implementations require that the server be made * aware of deletion of the local workspace, and this method provides an * opportunity for SCMs to perform such a clean-up act. * * <p> This call back is invoked after Hudson determines that a workspace is * unnecessary, but before the actual recursive directory deletion happens. * * <p> Note that this method does not guarantee that such a clean up will * happen. For example, slaves can be taken offline by being physically * removed from the network, and in such a case there's no opportunity to * perform this clean up. * * <p> This method is also invoked when the project is deleted. * * @param project The project that owns this {@link SCM}. This is always the * same object for a particular instance of {@link SCM}. Just passed in here * so that {@link SCM} itself doesn't have to remember the value. * @param workspace The workspace which is about to be deleted. Never null. * This can be a remote file path. * @param node The node that hosts the workspace. SCM can use this * information to determine the course of action. * * @return true if {@link SCM} is OK to let Hudson proceed with deleting the * workspace. False to veto the workspace deletion. * * @since 1.246 */ public boolean processWorkspaceBeforeDeletion(AbstractProject<?, ?> project, FilePath workspace, Node node) throws IOException, InterruptedException { return true; } /** * Checks if there has been any changes to this module in the repository. * * TODO: we need to figure out a better way to communicate an error back, so * that we won't keep retrying the same node (for example a slave might be * down.) * * <p> If the SCM doesn't implement polling, have the * {@link #supportsPolling()} method return false. * * @param project The project to check for updates * @param launcher Abstraction of the machine where the polling will take * place. If SCM declares that * {@linkplain #requiresWorkspaceForPolling() the polling doesn't require a workspace}, * this parameter is null. * @param workspace The workspace directory that contains baseline files. If * SCM declares that * {@linkplain #requiresWorkspaceForPolling() the polling doesn't require a workspace}, * this parameter is null. * @param listener Logs during the polling should be sent here. * * @return true if the change is detected. * * @throws InterruptedException interruption is usually caused by the user * aborting the computation. this exception should be simply propagated all * the way up. * * @see #supportsPolling() * * @deprecated as of 1.345 Override * {@link #calcRevisionsFromBuild(AbstractBuild, Launcher, TaskListener)} * and * {@link #compareRemoteRevisionWith(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)} * for implementation. * * The implementation is now separated in two pieces, one that computes the * revision of the current workspace, and the other that computes the * revision of the remote repository. * * Call * {@link #poll(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)} * for use instead. */ public boolean pollChanges(AbstractProject<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException { // up until 1.336, this method was abstract, so everyone should have overridden this method // without calling super.pollChanges. So the compatibility implementation is purely for // new implementations that doesn't override this method. // not sure if this can be implemented any better return false; } /** * Calculates the {@link SCMRevisionState} that represents the state of the * workspace of the given build. * * <p> The returned object is then fed into the * {@link #compareRemoteRevisionWith(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)} * method as the baseline {@link SCMRevisionState} to determine if the build * is necessary. * * <p> This method is called after source code is checked out for the given * build (that is, after * {@link SCM#checkout(AbstractBuild, Launcher, FilePath, BuildListener, File)} * has finished successfully.) * * <p> The obtained object is added to the build as an {@link Action} for * later retrieval. As an optimization, {@link SCM} implementation can * choose to compute {@link SCMRevisionState} and add it as an action during * check out, in which case this method will not called. * * @param build The calculated {@link SCMRevisionState} is for the files * checked out in this build. Never null. If * {@link #requiresWorkspaceForPolling()} returns true, Hudson makes sure * that the workspace of this build is available and accessible by the * callee. * @param launcher Abstraction of the machine where the polling will take * place. If SCM declares that * {@linkplain #requiresWorkspaceForPolling() the polling doesn't require a workspace}, * this parameter is null. Otherwise never null. * @param listener Logs during the polling should be sent here. * * @return can be null. * * @throws InterruptedException interruption is usually caused by the user * aborting the computation. this exception should be simply propagated all * the way up. */ public abstract SCMRevisionState calcRevisionsFromBuild(AbstractBuild<?, ?> build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException; /** * A pointless function to work around what appears to be a HotSpot problem. * See HUDSON-5756 and bug 6933067 on BugParade for more details. */ public SCMRevisionState _calcRevisionsFromBuild(AbstractBuild<?, ?> build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { return calcRevisionsFromBuild(build, launcher, listener); } /** * Compares the current state of the remote repository against the given * baseline {@link SCMRevisionState}. * * <p> Conceptually, the act of polling is to take two states of the * repository and to compare them to see if there's any difference. In * practice, however, comparing two arbitrary repository states is an * expensive operation, so in this abstraction, we chose to mix (1) the act * of building up a repository state and (2) the act of comparing it with * the earlier state, so that SCM implementations can implement this more * easily. * * <p> Multiple invocations of this method may happen over time to make sure * that the remote repository is "quiet" before Hudson schedules a new * build. * * @param project The project to check for updates * @param launcher Abstraction of the machine where the polling will take * place. If SCM declares that * {@linkplain #requiresWorkspaceForPolling() the polling doesn't require a workspace}, * this parameter is null. * @param workspace The workspace directory that contains baseline files. If * SCM declares that * {@linkplain #requiresWorkspaceForPolling() the polling doesn't require a workspace}, * this parameter is null. * @param listener Logs during the polling should be sent here. * @param baseline The baseline of the comparison. This object is the return * value from earlier * {@link #compareRemoteRevisionWith(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)} * or * {@link #calcRevisionsFromBuild(AbstractBuild, Launcher, TaskListener)}. * * @return This method returns multiple values that are bundled together * into the {@link PollingResult} value type. {@link PollingResult#baseline} * should be the value of the baseline parameter, * {@link PollingResult#remote} is the current state of the remote * repository (this object only needs to be understandable to the future * invocations of this method), and {@link PollingResult#change} that * indicates the degree of changes found during the comparison. * * @throws InterruptedException interruption is usually caused by the user * aborting the computation. this exception should be simply propagated all * the way up. */ protected abstract PollingResult compareRemoteRevisionWith(AbstractProject<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException; /** * A pointless function to work around what appears to be a HotSpot problem. * See HUDSON-5756 and bug 6933067 on BugParade for more details. */ private PollingResult _compareRemoteRevisionWith(AbstractProject<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline2) throws IOException, InterruptedException { return compareRemoteRevisionWith(project, launcher, workspace, listener, baseline2); } /** * Convenience method for the caller to handle the backward compatibility * between pre 1.345 SCMs. */ public final PollingResult poll(AbstractProject<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException { // Ensure poll can't run during project delete. // Fix Bug 460866 - SCM polling appears to hang Hudson synchronized (project.getDeleteLock()) { if (!project.isDeleted()) { if (is1_346OrLater()) { // This is to work around HUDSON-5827 in a general way. // don't let the SCM.compareRemoteRevisionWith(...) see SCMRevisionState that it didn't produce. SCMRevisionState baseline2; if (baseline != SCMRevisionState.NONE) { baseline2 = baseline; } else { baseline2 = _calcRevisionsFromBuild(project.getLastBuild(), launcher, listener); } return _compareRemoteRevisionWith(project, launcher, workspace, listener, baseline2); } else { return pollChanges(project, launcher, workspace, listener) ? PollingResult.SIGNIFICANT : PollingResult.NO_CHANGES; } } else { return PollingResult.NO_CHANGES; } } } private boolean is1_346OrLater() { for (Class<?> c = getClass(); c != SCM.class; c = c.getSuperclass()) { try { c.getDeclaredMethod("compareRemoteRevisionWith", AbstractProject.class, Launcher.class, FilePath.class, TaskListener.class, SCMRevisionState.class); return true; } catch (NoSuchMethodException e) { } } return false; } /** * Obtains a fresh workspace of the module(s) into the specified directory * of the specified machine. * * <p> The "update" operation can be performed instead of a fresh checkout * if feasible. * * <p> This operation should also capture the information necessary to tag * the workspace later. * * @param launcher Abstracts away the machine that the files will be checked * out. * @param workspace a directory to check out the source code. May contain * left-over from the previous build. * @param changelogFile Upon a successful return, this file should capture * the changelog. When there's no change, this file should contain an empty * entry. See {@link #createEmptyChangeLog(File, BuildListener, String)}. * @return false if the operation fails. The error should be reported to the * listener. Otherwise return the changes included in this update (if this * was an update.) * * @throws InterruptedException interruption is usually caused by the user * aborting the build. this exception will cause the build to abort. */ public abstract boolean checkout(AbstractBuild<?, ?> build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException; /** * Adds environmental variables for the builds to the given map. * * <p> This can be used to propagate information from SCM to builds (for * example, SVN revision number.) * * <p> This method is invoked whenever someone does * {@link AbstractBuild#getEnvironment(TaskListener)}, which can be * before/after your checkout method is invoked. So if you are going to * provide information about check out (like SVN revision number that was * checked out), be prepared for the possibility that the check out hasn't * happened yet. */ public void buildEnvVars(AbstractBuild<?, ?> build, Map<String, String> env) { // default implementation is noop. } /** * Gets the top directory of the checked out module. * * <p> Often SCMs have to create a directory inside a workspace, which * creates directory layout like this: * * <pre> * workspace <- workspace root * +- xyz <- directory checked out by SCM * +- CVS * +- build.xml <- user file * </pre> * * <p> Many builders, like Ant or Maven, works off the specific user file at * the top of the checked out module (in the above case, that would be * <tt>xyz/build.xml</tt>), yet the builder doesn't know the "xyz" part; * that comes from SCM. * * <p> Collaboration between {@link Builder} and {@link SCM} allows Hudson * to find build.xml wihout asking the user to enter "xyz" again. * * <p> This method is for this purpose. It takes the workspace root as a * parameter, and expected to return the directory that was checked out from * SCM. * * <p> If this SCM is configured to create a directory, try to return that * directory so that builders can work seamlessly. * * <p> If SCM doesn't need to create any directory inside workspace, or in * any other tricky cases, it should revert to the default implementation, * which is to just return the parameter. * * @param workspace The workspace root directory. * @param build The build for which the module root is desired. This * parameter is null when existing legacy code calls deprecated * {@link #getModuleRoot(FilePath)}. Handle this situation gracefully if * your can, but otherwise you can just fail with an exception, too. * * @since 1.382 */ public FilePath getModuleRoot(FilePath workspace, AbstractBuild build) { // For backwards compatibility, call the one argument version of the method. return getModuleRoot(workspace); } /** * @deprecated since 1.382 Use/override * {@link #getModuleRoot(FilePath, AbstractBuild)} instead. */ public FilePath getModuleRoot(FilePath workspace) { if (Util.isOverridden(SCM.class, getClass(), "getModuleRoot", FilePath.class, AbstractBuild.class)) // if the subtype already implements newer getModuleRoot(FilePath,AbstractBuild), call that. { return getModuleRoot(workspace, null); } return workspace; } /** * Gets the top directories of all the checked out modules. * * <p> Some SCMs support checking out multiple modules inside a workspace, * which creates directory layout like this: * * <pre> * workspace <- workspace root * +- xyz <- directory checked out by SCM * +- .svn * +- build.xml <- user file * +- abc <- second module from different SCM root * +- .svn * +- build.xml <- user file * </pre> * * This method takes the workspace root as a parameter, and is expected to * return all the module roots that were checked out from SCM. * * <p> For normal SCMs, the array will be of length * <code>1</code> and it's contents will be identical to calling * {@link #getModuleRoot(FilePath, AbstractBuild)}. * * @param workspace The workspace root directory * @param build The build for which the module roots are desired. This * parameter is null when existing legacy code calls deprecated * {@link #getModuleRoot(FilePath)}. Handle this situation gracefully if * your can, but otherwise you can just fail with an exception, too. * * @return An array of all module roots. * @since 1.382 */ public FilePath[] getModuleRoots(FilePath workspace, AbstractBuild build) { if (Util.isOverridden(SCM.class, getClass(), "getModuleRoots", FilePath.class)) // if the subtype derives legacy getModuleRoots(FilePath), delegate to it { return getModuleRoots(workspace); } // otherwise the default implementation return new FilePath[]{getModuleRoot(workspace, build)}; } /** * @deprecated as of 1.382. Use/derive from * {@link #getModuleRoots(FilePath, AbstractBuild)} instead. */ public FilePath[] getModuleRoots(FilePath workspace) { if (Util.isOverridden(SCM.class, getClass(), "getModuleRoots", FilePath.class, AbstractBuild.class)) // if the subtype already derives newer getModuleRoots(FilePath,AbstractBuild), delegate to it { return getModuleRoots(workspace, null); } // otherwise the default implementation return new FilePath[]{getModuleRoot(workspace),}; } /** * The returned object will be used to parse <tt>changelog.xml</tt>. */ public abstract ChangeLogParser createChangeLogParser(); public SCMDescriptor<?> getDescriptor() { return (SCMDescriptor) Hudson.getInstance().getDescriptorOrDie(getClass()); } // // convenience methods // protected final boolean createEmptyChangeLog(File changelogFile, BuildListener listener, String rootTag) { FileWriter w = null; try { w = new FileWriter(changelogFile); w.write("<" + rootTag + "/>"); w.close(); return true; } catch (IOException e) { e.printStackTrace(listener.error(e.getMessage())); return false; } finally { IOUtils.closeQuietly(w); } } protected final String nullify(String s) { if (s == null) { return null; } if (s.trim().length() == 0) { return null; } return s; } public static final PermissionGroup PERMISSIONS = new PermissionGroup(SCM.class, Messages._SCM_Permissions_Title()); /** * Permission to create new tags. * * @since 1.171 */ public static final Permission TAG = new Permission(PERMISSIONS, "Tag", Messages._SCM_TagPermission_Description(), Permission.CREATE); /** * Returns all the registered {@link SCMDescriptor}s. */ public static DescriptorExtensionList<SCM, SCMDescriptor<?>> all() { return Hudson.getInstance().<SCM, SCMDescriptor<?>>getDescriptorList(SCM.class); } /** * Returns the list of {@link SCMDescriptor}s that are applicable to the * given project. */ public static List<SCMDescriptor<?>> _for(final AbstractProject project) { if (project == null) { return all(); } final Descriptor pd = Hudson.getInstance().getDescriptor((Class) project.getClass()); List<SCMDescriptor<?>> r = new ArrayList<SCMDescriptor<?>>(); for (SCMDescriptor<?> scmDescriptor : all()) { if (!scmDescriptor.isApplicable(project)) { continue; } if (pd instanceof AbstractProjectDescriptor) { AbstractProjectDescriptor apd = (AbstractProjectDescriptor) pd; if (!apd.isApplicable(scmDescriptor)) { continue; } } r.add(scmDescriptor); } return r; } }