RefSpec.java
/*
* 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();
}
}