| /* |
| * Copyright (C) 2008, 2013 Shawn O. Pearce <spearce@spearce.org> |
| * and other copyright owners as documented in the project's IP log. |
| * |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Distribution License v1.0 which |
| * accompanies this distribution, is reproduced below, and is |
| * available at http://www.eclipse.org/org/documents/edl-v10.php |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or |
| * without modification, are permitted provided that the following |
| * conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * |
| * - Neither the name of the Eclipse Foundation, Inc. nor the |
| * names of its contributors may be used to endorse or promote |
| * products derived from this software without specific prior |
| * written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package org.eclipse.jgit.transport; |
| |
| import java.io.Serializable; |
| import java.text.MessageFormat; |
| |
| import org.eclipse.jgit.internal.JGitText; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.util.References; |
| |
| /** |
| * Describes how refs in one repository copy into another repository. |
| * <p> |
| * A ref specification provides matching support and limited rules to rewrite a |
| * reference in one repository to another reference in another repository. |
| */ |
| public class RefSpec implements Serializable { |
| private static final long serialVersionUID = 1L; |
| |
| /** |
| * Suffix for wildcard ref spec component, that indicate matching all refs |
| * with specified prefix. |
| */ |
| public static final String WILDCARD_SUFFIX = "/*"; //$NON-NLS-1$ |
| |
| /** |
| * Check whether provided string is a wildcard ref spec component. |
| * |
| * @param s |
| * ref spec component - string to test. Can be null. |
| * @return true if provided string is a wildcard ref spec component. |
| */ |
| public static boolean isWildcard(String s) { |
| return s != null && s.contains("*"); //$NON-NLS-1$ |
| } |
| |
| /** Does this specification ask for forced updated (rewind/reset)? */ |
| private boolean force; |
| |
| /** Is this specification actually a wildcard match? */ |
| private boolean wildcard; |
| |
| /** |
| * How strict to be about wildcards. |
| * |
| * @since 4.5 |
| */ |
| public enum WildcardMode { |
| /** |
| * Reject refspecs with an asterisk on the source side and not the |
| * destination side or vice versa. This is the mode used by FetchCommand |
| * and PushCommand to create a one-to-one mapping between source and |
| * destination refs. |
| */ |
| REQUIRE_MATCH, |
| /** |
| * Allow refspecs with an asterisk on only one side. This can create a |
| * many-to-one mapping between source and destination refs, so |
| * expandFromSource and expandFromDestination are not usable in this |
| * mode. |
| */ |
| ALLOW_MISMATCH |
| } |
| /** Whether a wildcard is allowed on one side but not the other. */ |
| private WildcardMode allowMismatchedWildcards; |
| |
| /** Name of the ref(s) we would copy from. */ |
| private String srcName; |
| |
| /** Name of the ref(s) we would copy into. */ |
| private String dstName; |
| |
| /** |
| * Construct an empty RefSpec. |
| * <p> |
| * A newly created empty RefSpec is not suitable for use in most |
| * applications, as at least one field must be set to match a source name. |
| */ |
| public RefSpec() { |
| force = false; |
| wildcard = false; |
| srcName = Constants.HEAD; |
| dstName = null; |
| allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH; |
| } |
| |
| /** |
| * Parse a ref specification for use during transport operations. |
| * <p> |
| * Specifications are typically one of the following forms: |
| * <ul> |
| * <li><code>refs/heads/master</code></li> |
| * <li><code>refs/heads/master:refs/remotes/origin/master</code></li> |
| * <li><code>refs/heads/*:refs/remotes/origin/*</code></li> |
| * <li><code>+refs/heads/master</code></li> |
| * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li> |
| * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li> |
| * <li><code>+refs/pull/*/head:refs/remotes/origin/pr/*</code></li> |
| * <li><code>:refs/heads/master</code></li> |
| * </ul> |
| * |
| * If the wildcard mode allows mismatches, then these ref specs are also |
| * valid: |
| * <ul> |
| * <li><code>refs/heads/*</code></li> |
| * <li><code>refs/heads/*:refs/heads/master</code></li> |
| * </ul> |
| * |
| * @param spec |
| * string describing the specification. |
| * @param mode |
| * whether to allow a wildcard on one side without a wildcard on |
| * the other. |
| * @throws java.lang.IllegalArgumentException |
| * the specification is invalid. |
| * @since 4.5 |
| */ |
| public RefSpec(String spec, WildcardMode mode) { |
| this.allowMismatchedWildcards = mode; |
| String s = spec; |
| if (s.startsWith("+")) { //$NON-NLS-1$ |
| force = true; |
| s = s.substring(1); |
| } |
| |
| final int c = s.lastIndexOf(':'); |
| if (c == 0) { |
| s = s.substring(1); |
| if (isWildcard(s)) { |
| wildcard = true; |
| if (mode == WildcardMode.REQUIRE_MATCH) { |
| throw new IllegalArgumentException(MessageFormat |
| .format(JGitText.get().invalidWildcards, spec)); |
| } |
| } |
| dstName = checkValid(s); |
| } else if (c > 0) { |
| String src = s.substring(0, c); |
| String dst = s.substring(c + 1); |
| if (isWildcard(src) && isWildcard(dst)) { |
| // Both contain wildcard |
| wildcard = true; |
| } else if (isWildcard(src) || isWildcard(dst)) { |
| wildcard = true; |
| if (mode == WildcardMode.REQUIRE_MATCH) |
| throw new IllegalArgumentException(MessageFormat |
| .format(JGitText.get().invalidWildcards, spec)); |
| } |
| srcName = checkValid(src); |
| dstName = checkValid(dst); |
| } else { |
| if (isWildcard(s)) { |
| if (mode == WildcardMode.REQUIRE_MATCH) { |
| throw new IllegalArgumentException(MessageFormat |
| .format(JGitText.get().invalidWildcards, spec)); |
| } |
| wildcard = true; |
| } |
| srcName = checkValid(s); |
| } |
| } |
| |
| /** |
| * Parse a ref specification for use during transport operations. |
| * <p> |
| * Specifications are typically one of the following forms: |
| * <ul> |
| * <li><code>refs/heads/master</code></li> |
| * <li><code>refs/heads/master:refs/remotes/origin/master</code></li> |
| * <li><code>refs/heads/*:refs/remotes/origin/*</code></li> |
| * <li><code>+refs/heads/master</code></li> |
| * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li> |
| * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li> |
| * <li><code>+refs/pull/*/head:refs/remotes/origin/pr/*</code></li> |
| * <li><code>:refs/heads/master</code></li> |
| * </ul> |
| * |
| * @param spec |
| * string describing the specification. |
| * @throws java.lang.IllegalArgumentException |
| * the specification is invalid. |
| */ |
| public RefSpec(String spec) { |
| this(spec, WildcardMode.REQUIRE_MATCH); |
| } |
| |
| private RefSpec(RefSpec p) { |
| force = p.isForceUpdate(); |
| wildcard = p.isWildcard(); |
| srcName = p.getSource(); |
| dstName = p.getDestination(); |
| allowMismatchedWildcards = p.allowMismatchedWildcards; |
| } |
| |
| /** |
| * Check if this specification wants to forcefully update the destination. |
| * |
| * @return true if this specification asks for updates without merge tests. |
| */ |
| public boolean isForceUpdate() { |
| return force; |
| } |
| |
| /** |
| * Create a new RefSpec with a different force update setting. |
| * |
| * @param forceUpdate |
| * new value for force update in the returned instance. |
| * @return a new RefSpec with force update as specified. |
| */ |
| public RefSpec setForceUpdate(boolean forceUpdate) { |
| final RefSpec r = new RefSpec(this); |
| r.force = forceUpdate; |
| return r; |
| } |
| |
| /** |
| * Check if this specification is actually a wildcard pattern. |
| * <p> |
| * If this is a wildcard pattern then the source and destination names |
| * returned by {@link #getSource()} and {@link #getDestination()} will not |
| * be actual ref names, but instead will be patterns. |
| * |
| * @return true if this specification could match more than one ref. |
| */ |
| public boolean isWildcard() { |
| return wildcard; |
| } |
| |
| /** |
| * Get the source ref description. |
| * <p> |
| * During a fetch this is the name of the ref on the remote repository we |
| * are fetching from. During a push this is the name of the ref on the local |
| * repository we are pushing out from. |
| * |
| * @return name (or wildcard pattern) to match the source ref. |
| */ |
| public String getSource() { |
| return srcName; |
| } |
| |
| /** |
| * Create a new RefSpec with a different source name setting. |
| * |
| * @param source |
| * new value for source in the returned instance. |
| * @return a new RefSpec with source as specified. |
| * @throws java.lang.IllegalStateException |
| * There is already a destination configured, and the wildcard |
| * status of the existing destination disagrees with the |
| * wildcard status of the new source. |
| */ |
| public RefSpec setSource(String source) { |
| final RefSpec r = new RefSpec(this); |
| r.srcName = checkValid(source); |
| if (isWildcard(r.srcName) && r.dstName == null) |
| throw new IllegalStateException(JGitText.get().destinationIsNotAWildcard); |
| if (isWildcard(r.srcName) != isWildcard(r.dstName)) |
| throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch); |
| return r; |
| } |
| |
| /** |
| * Get the destination ref description. |
| * <p> |
| * During a fetch this is the local tracking branch that will be updated |
| * with the new ObjectId after fetching is complete. During a push this is |
| * the remote ref that will be updated by the remote's receive-pack process. |
| * <p> |
| * If null during a fetch no tracking branch should be updated and the |
| * ObjectId should be stored transiently in order to prepare a merge. |
| * <p> |
| * If null during a push, use {@link #getSource()} instead. |
| * |
| * @return name (or wildcard) pattern to match the destination ref. |
| */ |
| public String getDestination() { |
| return dstName; |
| } |
| |
| /** |
| * Create a new RefSpec with a different destination name setting. |
| * |
| * @param destination |
| * new value for destination in the returned instance. |
| * @return a new RefSpec with destination as specified. |
| * @throws java.lang.IllegalStateException |
| * There is already a source configured, and the wildcard status |
| * of the existing source disagrees with the wildcard status of |
| * the new destination. |
| */ |
| public RefSpec setDestination(String destination) { |
| final RefSpec r = new RefSpec(this); |
| r.dstName = checkValid(destination); |
| if (isWildcard(r.dstName) && r.srcName == null) |
| throw new IllegalStateException(JGitText.get().sourceIsNotAWildcard); |
| if (isWildcard(r.srcName) != isWildcard(r.dstName)) |
| throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch); |
| return r; |
| } |
| |
| /** |
| * Create a new RefSpec with a different source/destination name setting. |
| * |
| * @param source |
| * new value for source in the returned instance. |
| * @param destination |
| * new value for destination in the returned instance. |
| * @return a new RefSpec with destination as specified. |
| * @throws java.lang.IllegalArgumentException |
| * The wildcard status of the new source disagrees with the |
| * wildcard status of the new destination. |
| */ |
| public RefSpec setSourceDestination(final String source, |
| final String destination) { |
| if (isWildcard(source) != isWildcard(destination)) |
| throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch); |
| final RefSpec r = new RefSpec(this); |
| r.wildcard = isWildcard(source); |
| r.srcName = source; |
| r.dstName = destination; |
| return r; |
| } |
| |
| /** |
| * Does this specification's source description match the ref name? |
| * |
| * @param r |
| * ref name that should be tested. |
| * @return true if the names match; false otherwise. |
| */ |
| public boolean matchSource(String r) { |
| return match(r, getSource()); |
| } |
| |
| /** |
| * Does this specification's source description match the ref? |
| * |
| * @param r |
| * ref whose name should be tested. |
| * @return true if the names match; false otherwise. |
| */ |
| public boolean matchSource(Ref r) { |
| return match(r.getName(), getSource()); |
| } |
| |
| /** |
| * Does this specification's destination description match the ref name? |
| * |
| * @param r |
| * ref name that should be tested. |
| * @return true if the names match; false otherwise. |
| */ |
| public boolean matchDestination(String r) { |
| return match(r, getDestination()); |
| } |
| |
| /** |
| * Does this specification's destination description match the ref? |
| * |
| * @param r |
| * ref whose name should be tested. |
| * @return true if the names match; false otherwise. |
| */ |
| public boolean matchDestination(Ref r) { |
| return match(r.getName(), getDestination()); |
| } |
| |
| /** |
| * Expand this specification to exactly match a ref name. |
| * <p> |
| * Callers must first verify the passed ref name matches this specification, |
| * otherwise expansion results may be unpredictable. |
| * |
| * @param r |
| * a ref name that matched our source specification. Could be a |
| * wildcard also. |
| * @return a new specification expanded from provided ref name. Result |
| * specification is wildcard if and only if provided ref name is |
| * wildcard. |
| * @throws java.lang.IllegalStateException |
| * when the RefSpec was constructed with wildcard mode that |
| * doesn't require matching wildcards. |
| */ |
| public RefSpec expandFromSource(String r) { |
| if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) { |
| throw new IllegalStateException( |
| JGitText.get().invalidExpandWildcard); |
| } |
| return isWildcard() ? new RefSpec(this).expandFromSourceImp(r) : this; |
| } |
| |
| private RefSpec expandFromSourceImp(String name) { |
| final String psrc = srcName, pdst = dstName; |
| wildcard = false; |
| srcName = name; |
| dstName = expandWildcard(name, psrc, pdst); |
| return this; |
| } |
| |
| /** |
| * Expand this specification to exactly match a ref. |
| * <p> |
| * Callers must first verify the passed ref matches this specification, |
| * otherwise expansion results may be unpredictable. |
| * |
| * @param r |
| * a ref that matched our source specification. Could be a |
| * wildcard also. |
| * @return a new specification expanded from provided ref name. Result |
| * specification is wildcard if and only if provided ref name is |
| * wildcard. |
| * @throws java.lang.IllegalStateException |
| * when the RefSpec was constructed with wildcard mode that |
| * doesn't require matching wildcards. |
| */ |
| public RefSpec expandFromSource(Ref r) { |
| return expandFromSource(r.getName()); |
| } |
| |
| /** |
| * Expand this specification to exactly match a ref name. |
| * <p> |
| * Callers must first verify the passed ref name matches this specification, |
| * otherwise expansion results may be unpredictable. |
| * |
| * @param r |
| * a ref name that matched our destination specification. Could |
| * be a wildcard also. |
| * @return a new specification expanded from provided ref name. Result |
| * specification is wildcard if and only if provided ref name is |
| * wildcard. |
| * @throws java.lang.IllegalStateException |
| * when the RefSpec was constructed with wildcard mode that |
| * doesn't require matching wildcards. |
| */ |
| public RefSpec expandFromDestination(String r) { |
| if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) { |
| throw new IllegalStateException( |
| JGitText.get().invalidExpandWildcard); |
| } |
| return isWildcard() ? new RefSpec(this).expandFromDstImp(r) : this; |
| } |
| |
| private RefSpec expandFromDstImp(String name) { |
| final String psrc = srcName, pdst = dstName; |
| wildcard = false; |
| srcName = expandWildcard(name, pdst, psrc); |
| dstName = name; |
| return this; |
| } |
| |
| /** |
| * Expand this specification to exactly match a ref. |
| * <p> |
| * Callers must first verify the passed ref matches this specification, |
| * otherwise expansion results may be unpredictable. |
| * |
| * @param r |
| * a ref that matched our destination specification. |
| * @return a new specification expanded from provided ref name. Result |
| * specification is wildcard if and only if provided ref name is |
| * wildcard. |
| * @throws java.lang.IllegalStateException |
| * when the RefSpec was constructed with wildcard mode that |
| * doesn't require matching wildcards. |
| */ |
| public RefSpec expandFromDestination(Ref r) { |
| return expandFromDestination(r.getName()); |
| } |
| |
| private boolean match(String name, String s) { |
| if (s == null) |
| return false; |
| if (isWildcard(s)) { |
| int wildcardIndex = s.indexOf('*'); |
| String prefix = s.substring(0, wildcardIndex); |
| String suffix = s.substring(wildcardIndex + 1); |
| return name.length() > prefix.length() + suffix.length() |
| && name.startsWith(prefix) && name.endsWith(suffix); |
| } |
| return name.equals(s); |
| } |
| |
| private static String expandWildcard(String name, String patternA, |
| String patternB) { |
| int a = patternA.indexOf('*'); |
| int trailingA = patternA.length() - (a + 1); |
| int b = patternB.indexOf('*'); |
| String match = name.substring(a, name.length() - trailingA); |
| return patternB.substring(0, b) + match + patternB.substring(b + 1); |
| } |
| |
| private static String checkValid(String spec) { |
| if (spec != null && !isValid(spec)) |
| throw new IllegalArgumentException(MessageFormat.format( |
| JGitText.get().invalidRefSpec, spec)); |
| return spec; |
| } |
| |
| private static boolean isValid(String s) { |
| if (s.startsWith("/")) //$NON-NLS-1$ |
| return false; |
| if (s.contains("//")) //$NON-NLS-1$ |
| return false; |
| if (s.endsWith("/")) //$NON-NLS-1$ |
| return false; |
| int i = s.indexOf('*'); |
| if (i != -1) { |
| if (s.indexOf('*', i + 1) > i) |
| return false; |
| } |
| return true; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public int hashCode() { |
| int hc = 0; |
| if (getSource() != null) |
| hc = hc * 31 + getSource().hashCode(); |
| if (getDestination() != null) |
| hc = hc * 31 + getDestination().hashCode(); |
| return hc; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof RefSpec)) |
| return false; |
| final RefSpec b = (RefSpec) obj; |
| if (isForceUpdate() != b.isForceUpdate()) |
| return false; |
| if (isWildcard() != b.isWildcard()) |
| return false; |
| if (!eq(getSource(), b.getSource())) |
| return false; |
| if (!eq(getDestination(), b.getDestination())) |
| return false; |
| return true; |
| } |
| |
| private static boolean eq(String a, String b) { |
| if (References.isSameObject(a, b)) { |
| return true; |
| } |
| if (a == null || b == null) |
| return false; |
| return a.equals(b); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public String toString() { |
| final StringBuilder r = new StringBuilder(); |
| if (isForceUpdate()) |
| r.append('+'); |
| if (getSource() != null) |
| r.append(getSource()); |
| if (getDestination() != null) { |
| r.append(':'); |
| r.append(getDestination()); |
| } |
| return r.toString(); |
| } |
| } |