/* * The MIT License * * Copyright (c) 2011-2013, CloudBees, Inc., Stephen Connolly. * * 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.api; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.FilePath; import hudson.Launcher; import hudson.model.Item; import hudson.model.Run; import hudson.model.TaskListener; import hudson.scm.SCM; import hudson.scm.SCMRevisionState; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.OutputStream; /** * A virtual file system for a specific {@link SCM} potentially pinned to a specific {@link SCMRevision}. In contrast * to {@link SCMProbe}, implementations should not cache results between {@link SCMFileSystem} instantiations. * <p> * While some DVCS implementations may need to perform a local checkout in order to be able to implement this API it * should be noted that in such cases the local checkout is not a cache but rather a copy of the immutable revisions * - this may look and sound like a cache but it isn't as the revision itself is immutable. When a new * {@link SCMFileSystem} if being instantiated against a {@code null} {@link SCMRevision} the DVCS system can re-use * the previous local checkout <em>after reconfirming that the current revision for the head matches that of the local * checkout.</em> * <p> * Where the {@link #getRevision()} is {@code null} or {@link SCMRevision#isDeterministic()} a {@link SCMFileSystem} * can choose to keep the results locally (up to {@link SCMFileSystem#close()}) or re-query against the remote. * * @author Stephen Connolly */ public abstract class SCMFileSystem implements Closeable { /** * The revision that this file system is pinned on. */ @CheckForNull private final SCMRevision rev; /** * Constructor. * * @param rev the revision. */ protected SCMFileSystem(@CheckForNull SCMRevision rev) { this.rev = rev; } /** * {@inheritDoc} */ @Override public void close() throws IOException { // no-op } /** * Returns the time that the {@link SCMFileSystem} was last modified. This should logically be equivalent to the * maximum {@link SCMFile#lastModified()} that you would find if you were to do the horribly inefficient traversal * of all the {@link SCMFile} instances from {@link #getRoot()}. Where implementers do not have an easy and quick * way to get this information (such as by looking at the commit time of the {@link #getRevision()} HINT HINT) * then just return {@code 0L}. * * @return A <code>long</code> value representing the time the {@link SCMFileSystem} was * last modified, measured in milliseconds since the epoch * (00:00:00 GMT, January 1, 1970) or {@code 0L} if the operation is unsupported. * @throws IOException if an error occurs while performing the operation. * @throws InterruptedException if interrupted while performing the operation. */ public abstract long lastModified() throws IOException, InterruptedException; /** * If this inspector is looking at the specific commit, * returns that revision. Otherwise null, indicating * that the inspector is looking at just the latest state * of the repository. * * @return the revision of the commit the inspector is looking at, or null if none. */ @CheckForNull public SCMRevision getRevision() { return rev; } /** * Whether this inspector is looking at the specific commit. * <p>Short for {@code getRevision()!=null}.</p>. * * @return true if this inspector is looking at the specific commit. */ public final boolean isFixedRevision() { return getRevision() != null; } /** * Short for {@code getRoot().child(path)}. * * @param path Path of the SCMFile to obtain from the root of the repository. * @return null if there's no file/directory at the requested path. */ @NonNull public final SCMFile child(@NonNull String path) { return getRoot().child(path); } /** * Returns the {@link SCMFile} object that represents the root directory of the repository. * * @return the root directory of the repository. */ @NonNull public abstract SCMFile getRoot(); /** * Writes the changes between the specified revision and {@link #getRevision()} in the format compatible * with the {@link SCM} from this {@link SCMFileSystem#of(Item, SCM)} to the supplied {@link OutputStream}. * This method allows for consumers or the SCM API to replicate the * {@link SCM#checkout(Run, Launcher, FilePath, TaskListener, File, SCMRevisionState)} functionality * that captures the changelog without requiring a full checkout. * * @param revision the starting revision or {@code null} to capture the initial change set. * @param changeLogStream the destination to stream the changes to. * @return {@code true} if there are changes, {@code false} if there were no changes. * @throws UnsupportedOperationException if this {@link SCMFileSystem} does not support changelog querying. * @throws IOException if an error occurs while performing the operation. * @throws InterruptedException if interrupted while performing the operation. * @since 2.0 */ public boolean changesSince(@CheckForNull SCMRevision revision, @NonNull OutputStream changeLogStream) throws UnsupportedOperationException, IOException, InterruptedException { throw new UnsupportedOperationException(); } /** * Given a {@link SCM} this method will try to retrieve a corresponding {@link SCMFileSystem} instance. * * @param owner the owner of the {@link SCM} * @param scm the {@link SCM}. * @return the corresponding {@link SCMFileSystem} or {@code null} if there is none. * @throws IOException if the attempt to create a {@link SCMFileSystem} failed due to an IO error * (such as the remote system being unavailable) * @throws InterruptedException if the attempt to create a {@link SCMFileSystem} was interrupted. */ @CheckForNull public static SCMFileSystem of(@NonNull Item owner, @NonNull SCM scm) throws IOException, InterruptedException { return of(owner, scm, null); } /** * Given a {@link SCM} this method will try to retrieve a corresponding {@link SCMFileSystem} instance that * reflects the content at the specified {@link SCMRevision}. * * @param owner the owner of the {@link SCM} * @param scm the {@link SCM}. * @param rev the specified {@link SCMRevision}. * @return the corresponding {@link SCMFileSystem} or {@code null} if there is none. * @throws IOException if the attempt to create a {@link SCMFileSystem} failed due to an IO error * (such as the remote system being unavailable) * @throws InterruptedException if the attempt to create a {@link SCMFileSystem} was interrupted. */ @CheckForNull public static SCMFileSystem of(@NonNull Item owner, @NonNull SCM scm, @CheckForNull SCMRevision rev) throws IOException, InterruptedException { scm.getClass(); // throw NPE if null SCMFileSystem fallBack = null; Throwable failure = null; for (Builder b : ExtensionList.lookup(Builder.class)) { if (b.supports(scm)) { try { SCMFileSystem inspector = b.build(owner, scm, rev); if (inspector != null) { if (inspector.isFixedRevision()) { return inspector; } if (fallBack == null) { fallBack = inspector; } } } catch (IOException e) { if (failure == null) { failure = e; } // TODO else { failure.addSuppressed(e); } // once Java 7 } catch (InterruptedException e) { if (failure == null) { failure = e; } // TODO else { failure.addSuppressed(e); } // once Java 7 } catch (RuntimeException e) { if (failure == null) { failure = e; } // TODO else { failure.addSuppressed(e); } // once Java 7 } } } if (fallBack == null) { if (failure instanceof IOException) { throw (IOException) failure; } if (failure instanceof InterruptedException) { throw (InterruptedException) failure; } //noinspection ConstantConditions if (failure instanceof RuntimeException) { throw (RuntimeException) failure; } } return fallBack; } /** * Given a {@link SCM} this method will check if there is at least one {@link SCMFileSystem} provider capable * of being instantiated. Returning {@code true} does not mean that {@link #of(Item, SCM, SCMRevision)} * will be able to instantiate a {@link SCMFileSystem} for any specific {@link SCMRevision}, * rather returning {@code false} indicates that there is absolutely no point in calling * {@link #of(Item, SCM, SCMRevision)} as it will always return {@code null}. * * @param scm the {@link SCMSource}. * @return {@code true} if {@link SCMFileSystem#of(Item, SCM)} / {@link #of(Item, SCM, SCMRevision)} could return a * {@link SCMFileSystem} implementation, {@code false} if {@link SCMFileSystem#of(Item, SCM)} / * {@link #of(Item, SCM, SCMRevision)} will always return {@code null} for the supplied {@link SCM}. * @since 2.0 */ public static boolean supports(@NonNull SCM scm) { scm.getClass(); // throw NPE if null for (Builder b : ExtensionList.lookup(Builder.class)) { if (b.supports(scm)) { return true; } } return false; } /** * Given a {@link SCMSource} and a {@link SCMHead} this method will try to retrieve a corresponding * {@link SCMFileSystem} instance that reflects the content of the specified {@link SCMHead}. * * @param source the {@link SCMSource}. * @param head the specified {@link SCMHead}. * @return the corresponding {@link SCMFileSystem} or {@code null} if there is none. * @throws IOException if the attempt to create a {@link SCMFileSystem} failed due to an IO error * (such as the remote system being unavailable) * @throws InterruptedException if the attempt to create a {@link SCMFileSystem} was interrupted. */ @CheckForNull public static SCMFileSystem of(@NonNull SCMSource source, @NonNull SCMHead head) throws IOException, InterruptedException { return of(source, head, null); } /** * Given a {@link SCMSource}, a {@link SCMHead} and a {@link SCMRevision} this method will try to retrieve a * corresponding {@link SCMFileSystem} instance that reflects the content of the specified {@link SCMHead} at the * specified {@link SCMRevision}. * * @param source the {@link SCMSource}. * @param head the specified {@link SCMHead}. * @param rev the specified {@link SCMRevision}. * @return the corresponding {@link SCMFileSystem} or {@code null} if there is none. * @throws IOException if the attempt to create a {@link SCMFileSystem} failed due to an IO error * (such as the remote system being unavailable) * @throws InterruptedException if the attempt to create a {@link SCMFileSystem} was interrupted. */ @CheckForNull public static SCMFileSystem of(@NonNull SCMSource source, @NonNull SCMHead head, @CheckForNull SCMRevision rev) throws IOException, InterruptedException { source.getClass(); // throw NPE if null SCMFileSystem fallBack = null; Throwable failure = null; for (Builder b : ExtensionList.lookup(Builder.class)) { if (b.supports(source)) { try { SCMFileSystem inspector = b.build(source, head, rev); if (inspector != null) { if (inspector.isFixedRevision()) { return inspector; } if (fallBack == null) { fallBack = inspector; } } } catch (IOException e) { if (failure == null) { failure = e; } // TODO else { failure.addSuppressed(e); } // once Java 7 } catch (InterruptedException e) { if (failure == null) { failure = e; } // TODO else { failure.addSuppressed(e); } // once Java 7 } catch (RuntimeException e) { if (failure == null) { failure = e; } // TODO else { failure.addSuppressed(e); } // once Java 7 } } } if (fallBack == null) { if (failure instanceof IOException) { throw (IOException) failure; } if (failure instanceof InterruptedException) { throw (InterruptedException) failure; } //noinspection ConstantConditions if (failure instanceof RuntimeException) { throw (RuntimeException) failure; } } return fallBack; } /** * Given a {@link SCMSource} this method will check if there is at least one {@link SCMFileSystem} provider capable * of being instantiated. Returning {@code true} does not mean that {@link #of(SCMSource, SCMHead, SCMRevision)} * will be able to instantiate a {@link SCMFileSystem} for any specific {@link SCMHead} or {@link SCMRevision}, * rather returning {@code false} indicates that there is absolutely no point in calling * {@link #of(SCMSource, SCMHead, SCMRevision)} as it will always return {@code null}. * * @param source the {@link SCMSource}. * @return {@code true} if {@link #of(SCMSource, SCMHead)} / {@link #of(SCMSource, SCMHead, SCMRevision)} could * return a {@link SCMFileSystem} implementation, {@code false} if {@link #of(SCMSource, SCMHead)} / * {@link #of(SCMSource, SCMHead, SCMRevision)} will always return {@code null} for the supplied {@link SCMSource}. * @since 2.0 */ public static boolean supports(@NonNull SCMSource source) { source.getClass(); // throw NPE if null for (Builder b : ExtensionList.lookup(Builder.class)) { if (b.supports(source)) { return true; } } return false; } /** * Extension point that allows different plugins to implement {@link SCMFileSystem} classes for the same {@link SCM} * or {@link SCMSource} and let Jenkins pick the most capable for any specific {@link SCM} implementation. */ public abstract static class Builder implements ExtensionPoint { /** * Checks if this {@link Builder} supports the supplied {@link SCM}. * * @param source the {@link SCM}. * @return {@code true} if and only if the supplied {@link SCM} is supported by this {@link Builder}, * {@code false} if {@link #build(Item, SCM, SCMRevision)} will <strong>always</strong> return {@code null}. */ public abstract boolean supports(SCM source); /** * Checks if this {@link Builder} supports the supplied {@link SCMSource}. * * @param source the {@link SCMSource}. * @return {@code true} if and only if the supplied {@link SCMSource} is supported by this {@link Builder}, * {@code false} if {@link #build(SCMSource, SCMHead, SCMRevision)} will <strong>always</strong> return * {@code null}. */ public abstract boolean supports(SCMSource source); /** * Given a {@link SCM} this should try to build a corresponding {@link SCMFileSystem} instance that * reflects the content at the specified {@link SCMRevision}. If the {@link SCM} is supported but not * for a fixed revision, best effort is acceptable as the most capable {@link SCMFileSystem} will be returned * to the caller. * * @param owner the owner of the {@link SCM} * @param scm the {@link SCM}. * @param rev the specified {@link SCMRevision}. * @return the corresponding {@link SCMFileSystem} or {@code null} if this builder cannot create a {@link * SCMFileSystem} for the specified {@link SCM}. * @throws IOException if the attempt to create a {@link SCMFileSystem} failed due to an IO error * (such as the remote system being unavailable) * @throws InterruptedException if the attempt to create a {@link SCMFileSystem} was interrupted. */ @CheckForNull public abstract SCMFileSystem build(@NonNull Item owner, @NonNull SCM scm, @CheckForNull SCMRevision rev) throws IOException, InterruptedException; /** * Given a {@link SCMSource}, a {@link SCMHead} and a {@link SCMRevision} this method should try to build a * corresponding {@link SCMFileSystem} instance that reflects the content of the specified {@link SCMHead} at * the specified {@link SCMRevision}. If the {@link SCMSource} is supported but not for a fixed revision, * best effort is acceptable as the most capable {@link SCMFileSystem} will be returned * to the caller. * * @param source the {@link SCMSource}. * @param head the specified {@link SCMHead}. * @param rev the specified {@link SCMRevision}. * @return the corresponding {@link SCMFileSystem} or {@code null} if there is none. * @throws IOException if the attempt to create a {@link SCMFileSystem} failed due to an IO error * (such as the remote system being unavailable) * @throws InterruptedException if the attempt to create a {@link SCMFileSystem} was interrupted. */ @CheckForNull public SCMFileSystem build(@NonNull SCMSource source, @NonNull SCMHead head, @CheckForNull SCMRevision rev) throws IOException, InterruptedException { SCMSourceOwner owner = source.getOwner(); if (owner == null) { throw new IOException("Cannot instantiate a SCMFileSystem from an SCM without an owner"); } return build(owner, source.build(head, rev), rev); } } }