| /* |
| * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Distribution License v. 1.0 which is available at |
| * https://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| |
| package org.eclipse.jgit.transport; |
| |
| import java.io.Serializable; |
| import java.text.MessageFormat; |
| import java.util.Objects; |
| |
| import org.eclipse.jgit.internal.JGitText; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.Ref; |
| |
| /** |
| * 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; |
| |
| /** Is this the special ":" RefSpec? */ |
| private boolean matching; |
| |
| /** Is this a negative refspec. */ |
| private boolean negative; |
| |
| /** |
| * 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() { |
| matching = false; |
| force = false; |
| wildcard = false; |
| srcName = Constants.HEAD; |
| dstName = null; |
| negative =false; |
| allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH; |
| } |
| |
| /** |
| * Parse a ref specification for use during transport operations. |
| * <p> |
| * {@link RefSpec}s can be regular or negative, regular RefSpecs indicate |
| * what to include in transport operations while negative RefSpecs indicate |
| * what to exclude in fetch. |
| * <p> |
| * Negative {@link RefSpec}s can't be force, must have only source or |
| * destination. Wildcard patterns are also supported in negative RefSpecs |
| * but they can not go with {@code WildcardMode.REQUIRE_MATCH} because they |
| * are natually one to many mappings. |
| * |
| * <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> |
| * |
| * Negative specifications are usually like: |
| * <ul> |
| * <li><code>^:refs/heads/master</code></li> |
| * <li><code>^refs/heads/*</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("^+") || s.startsWith("+^")) { //$NON-NLS-1$ //$NON-NLS-2$ |
| throw new IllegalArgumentException( |
| JGitText.get().invalidNegativeAndForce); |
| } |
| |
| if (s.startsWith("+")) { //$NON-NLS-1$ |
| force = true; |
| s = s.substring(1); |
| } |
| |
| if (s.startsWith("^")) { //$NON-NLS-1$ |
| negative = true; |
| s = s.substring(1); |
| } |
| |
| boolean matchPushSpec = false; |
| final int c = s.lastIndexOf(':'); |
| if (c == 0) { |
| s = s.substring(1); |
| if (s.isEmpty()) { |
| matchPushSpec = true; |
| wildcard = true; |
| srcName = Constants.R_HEADS + '*'; |
| dstName = srcName; |
| } else { |
| 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); |
| } |
| |
| // Negative refspecs must only have dstName or srcName. |
| if (isNegative()) { |
| if (isNullOrEmpty(srcName) && isNullOrEmpty(dstName)) { |
| throw new IllegalArgumentException(MessageFormat |
| .format(JGitText.get().invalidRefSpec, spec)); |
| } |
| if (!isNullOrEmpty(srcName) && !isNullOrEmpty(dstName)) { |
| throw new IllegalArgumentException(MessageFormat |
| .format(JGitText.get().invalidRefSpec, spec)); |
| } |
| if(wildcard && mode == WildcardMode.REQUIRE_MATCH) { |
| throw new IllegalArgumentException(MessageFormat |
| .format(JGitText.get().invalidRefSpec, spec));} |
| } |
| matching = matchPushSpec; |
| } |
| |
| /** |
| * 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, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH //$NON-NLS-1$ |
| : WildcardMode.REQUIRE_MATCH); |
| } |
| |
| private RefSpec(RefSpec p) { |
| matching = false; |
| force = p.isForceUpdate(); |
| wildcard = p.isWildcard(); |
| negative = p.isNegative(); |
| srcName = p.getSource(); |
| dstName = p.getDestination(); |
| allowMismatchedWildcards = p.allowMismatchedWildcards; |
| } |
| |
| /** |
| * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":" |
| * for pushing. |
| * |
| * @return whether this is a "matching" RefSpec |
| * @since 6.1 |
| */ |
| public boolean isMatching() { |
| return matching; |
| } |
| |
| /** |
| * 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); |
| if (forceUpdate && isNegative()) { |
| throw new IllegalArgumentException( |
| JGitText.get().invalidNegativeAndForce); |
| } |
| r.matching = matching; |
| 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; |
| } |
| |
| /** |
| * Check if this specification is a negative one. |
| * |
| * @return true if this specification is negative. |
| * @since 6.2 |
| */ |
| public boolean isNegative() { |
| return negative; |
| } |
| |
| /** |
| * 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(String source, 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; |
| } |
| |
| private static boolean isNullOrEmpty(String refName) { |
| return refName == null || refName.isEmpty(); |
| } |
| |
| /** |
| * 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; |
| } |
| |
| @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; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof RefSpec)) |
| return false; |
| final RefSpec b = (RefSpec) obj; |
| if (isForceUpdate() != b.isForceUpdate()) { |
| return false; |
| } |
| if(isNegative() != b.isNegative()) { |
| return false; |
| } |
| if (isMatching()) { |
| return b.isMatching(); |
| } else if (b.isMatching()) { |
| return false; |
| } |
| return isWildcard() == b.isWildcard() |
| && Objects.equals(getSource(), b.getSource()) |
| && Objects.equals(getDestination(), b.getDestination()); |
| } |
| |
| @Override |
| public String toString() { |
| final StringBuilder r = new StringBuilder(); |
| if (isForceUpdate()) { |
| r.append('+'); |
| } |
| if(isNegative()) { |
| r.append('^'); |
| } |
| if (isMatching()) { |
| r.append(':'); |
| } else { |
| if (getSource() != null) { |
| r.append(getSource()); |
| } |
| if (getDestination() != null) { |
| r.append(':'); |
| r.append(getDestination()); |
| } |
| } |
| return r.toString(); |
| } |
| } |