View Javadoc
1   /*
2    * Copyright (C) 2008, 2013 Shawn O. Pearce <spearce@spearce.org> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.transport;
12  
13  import java.io.Serializable;
14  import java.text.MessageFormat;
15  import java.util.Objects;
16  
17  import org.eclipse.jgit.internal.JGitText;
18  import org.eclipse.jgit.lib.Constants;
19  import org.eclipse.jgit.lib.Ref;
20  
21  /**
22   * Describes how refs in one repository copy into another repository.
23   * <p>
24   * A ref specification provides matching support and limited rules to rewrite a
25   * reference in one repository to another reference in another repository.
26   */
27  public class RefSpec implements Serializable {
28  	private static final long serialVersionUID = 1L;
29  
30  	/**
31  	 * Suffix for wildcard ref spec component, that indicate matching all refs
32  	 * with specified prefix.
33  	 */
34  	public static final String WILDCARD_SUFFIX = "/*"; //$NON-NLS-1$
35  
36  	/**
37  	 * Check whether provided string is a wildcard ref spec component.
38  	 *
39  	 * @param s
40  	 *            ref spec component - string to test. Can be null.
41  	 * @return true if provided string is a wildcard ref spec component.
42  	 */
43  	public static boolean isWildcard(String s) {
44  		return s != null && s.contains("*"); //$NON-NLS-1$
45  	}
46  
47  	/** Does this specification ask for forced updated (rewind/reset)? */
48  	private boolean force;
49  
50  	/** Is this specification actually a wildcard match? */
51  	private boolean wildcard;
52  
53  	/** Is this the special ":" RefSpec? */
54  	private boolean matching;
55  
56  	/** Is this a negative refspec. */
57  	private boolean negative;
58  
59  	/**
60  	 * How strict to be about wildcards.
61  	 *
62  	 * @since 4.5
63  	 */
64  	public enum WildcardMode {
65  		/**
66  		 * Reject refspecs with an asterisk on the source side and not the
67  		 * destination side or vice versa. This is the mode used by FetchCommand
68  		 * and PushCommand to create a one-to-one mapping between source and
69  		 * destination refs.
70  		 */
71  		REQUIRE_MATCH,
72  		/**
73  		 * Allow refspecs with an asterisk on only one side. This can create a
74  		 * many-to-one mapping between source and destination refs, so
75  		 * expandFromSource and expandFromDestination are not usable in this
76  		 * mode.
77  		 */
78  		ALLOW_MISMATCH
79  	}
80  
81  	/** Whether a wildcard is allowed on one side but not the other. */
82  	private WildcardMode allowMismatchedWildcards;
83  
84  	/** Name of the ref(s) we would copy from. */
85  	private String srcName;
86  
87  	/** Name of the ref(s) we would copy into. */
88  	private String dstName;
89  
90  	/**
91  	 * Construct an empty RefSpec.
92  	 * <p>
93  	 * A newly created empty RefSpec is not suitable for use in most
94  	 * applications, as at least one field must be set to match a source name.
95  	 */
96  	public RefSpec() {
97  		matching = false;
98  		force = false;
99  		wildcard = false;
100 		srcName = Constants.HEAD;
101 		dstName = null;
102 		negative =false;
103 		allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH;
104 	}
105 
106 	/**
107 	 * Parse a ref specification for use during transport operations.
108 	 * <p>
109 	 * {@link RefSpec}s can be regular or negative, regular RefSpecs indicate
110 	 * what to include in transport operations while negative RefSpecs indicate
111 	 * what to exclude in fetch.
112 	 * <p>
113 	 * Negative {@link RefSpec}s can't be force, must have only source or
114 	 * destination. Wildcard patterns are also supported in negative RefSpecs
115 	 * but they can not go with {@code WildcardMode.REQUIRE_MATCH} because they
116 	 * are natually one to many mappings.
117 	 *
118 	 * <p>
119 	 * Specifications are typically one of the following forms:
120 	 * <ul>
121 	 * <li><code>refs/heads/master</code></li>
122 	 * <li><code>refs/heads/master:refs/remotes/origin/master</code></li>
123 	 * <li><code>refs/heads/*:refs/remotes/origin/*</code></li>
124 	 * <li><code>+refs/heads/master</code></li>
125 	 * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li>
126 	 * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li>
127 	 * <li><code>+refs/pull/&#42;/head:refs/remotes/origin/pr/*</code></li>
128 	 * <li><code>:refs/heads/master</code></li>
129 	 * </ul>
130 	 *
131 	 * If the wildcard mode allows mismatches, then these ref specs are also
132 	 * valid:
133 	 * <ul>
134 	 * <li><code>refs/heads/*</code></li>
135 	 * <li><code>refs/heads/*:refs/heads/master</code></li>
136 	 * </ul>
137 	 *
138 	 * Negative specifications are usually like:
139 	 * <ul>
140 	 * <li><code>^:refs/heads/master</code></li>
141 	 * <li><code>^refs/heads/*</code></li>
142 	 * </ul>
143 	 *
144 	 * @param spec
145 	 *            string describing the specification.
146 	 * @param mode
147 	 *            whether to allow a wildcard on one side without a wildcard on
148 	 *            the other.
149 	 * @throws java.lang.IllegalArgumentException
150 	 *             the specification is invalid.
151 	 * @since 4.5
152 	 */
153 	public RefSpec(String spec, WildcardMode mode) {
154 		this.allowMismatchedWildcards = mode;
155 		String s = spec;
156 
157 		if (s.startsWith("^+") || s.startsWith("+^")) {
158 			throw new IllegalArgumentException(
159 					JGitText.get().invalidNegativeAndForce);
160 		}
161 
162 		if (s.startsWith("+")) { //$NON-NLS-1$
163 			force = true;
164 			s = s.substring(1);
165 		}
166 
167 		if(s.startsWith("^")) {
168 			negative = true;
169 			s = s.substring(1);
170 		}
171 
172 		boolean matchPushSpec = false;
173 		final int c = s.lastIndexOf(':');
174 		if (c == 0) {
175 			s = s.substring(1);
176 			if (s.isEmpty()) {
177 				matchPushSpec = true;
178 				wildcard = true;
179 				srcName = Constants.R_HEADS + '*';
180 				dstName = srcName;
181 			} else {
182 				if (isWildcard(s)) {
183 					wildcard = true;
184 					if (mode == WildcardMode.REQUIRE_MATCH) {
185 						throw new IllegalArgumentException(MessageFormat
186 								.format(JGitText.get().invalidWildcards, spec));
187 					}
188 				}
189 				dstName = checkValid(s);
190 			}
191 		} else if (c > 0) {
192 			String src = s.substring(0, c);
193 			String dst = s.substring(c + 1);
194 			if (isWildcard(src) && isWildcard(dst)) {
195 				// Both contain wildcard
196 				wildcard = true;
197 			} else if (isWildcard(src) || isWildcard(dst)) {
198 				wildcard = true;
199 				if (mode == WildcardMode.REQUIRE_MATCH)
200 					throw new IllegalArgumentException(MessageFormat
201 							.format(JGitText.get().invalidWildcards, spec));
202 			}
203 			srcName = checkValid(src);
204 			dstName = checkValid(dst);
205 		} else {
206 			if (isWildcard(s)) {
207 				if (mode == WildcardMode.REQUIRE_MATCH) {
208 					throw new IllegalArgumentException(MessageFormat
209 							.format(JGitText.get().invalidWildcards, spec));
210 				}
211 				wildcard = true;
212 			}
213 			srcName = checkValid(s);
214 		}
215 
216 		// Negative refspecs must only have dstName or srcName.
217 		if (isNegative()) {
218 			if (isNullOrEmpty(srcName) && isNullOrEmpty(dstName)) {
219 				throw new IllegalArgumentException(MessageFormat
220 						.format(JGitText.get().invalidRefSpec, spec));
221 			}
222 			if (!isNullOrEmpty(srcName) && !isNullOrEmpty(dstName)) {
223 				throw new IllegalArgumentException(MessageFormat
224 						.format(JGitText.get().invalidRefSpec, spec));
225 			}
226 			if(wildcard && mode == WildcardMode.REQUIRE_MATCH) {
227 				throw new IllegalArgumentException(MessageFormat
228 						.format(JGitText.get().invalidRefSpec, spec));}
229 		}
230 		matching = matchPushSpec;
231 	}
232 
233 	/**
234 	 * Parse a ref specification for use during transport operations.
235 	 * <p>
236 	 * Specifications are typically one of the following forms:
237 	 * <ul>
238 	 * <li><code>refs/heads/master</code></li>
239 	 * <li><code>refs/heads/master:refs/remotes/origin/master</code></li>
240 	 * <li><code>refs/heads/*:refs/remotes/origin/*</code></li>
241 	 * <li><code>+refs/heads/master</code></li>
242 	 * <li><code>+refs/heads/master:refs/remotes/origin/master</code></li>
243 	 * <li><code>+refs/heads/*:refs/remotes/origin/*</code></li>
244 	 * <li><code>+refs/pull/&#42;/head:refs/remotes/origin/pr/*</code></li>
245 	 * <li><code>:refs/heads/master</code></li>
246 	 * </ul>
247 	 *
248 	 * @param spec
249 	 *            string describing the specification.
250 	 * @throws java.lang.IllegalArgumentException
251 	 *             the specification is invalid.
252 	 */
253 	public RefSpec(String spec) {
254 		this(spec, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH
255 				: WildcardMode.REQUIRE_MATCH);
256 	}
257 
258 	private RefSpec(RefSpec p) {
259 		matching = false;
260 		force = p.isForceUpdate();
261 		wildcard = p.isWildcard();
262 		negative = p.isNegative();
263 		srcName = p.getSource();
264 		dstName = p.getDestination();
265 		allowMismatchedWildcards = p.allowMismatchedWildcards;
266 	}
267 
268 	/**
269 	 * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":"
270 	 * for pushing.
271 	 *
272 	 * @return whether this is a "matching" RefSpec
273 	 * @since 6.1
274 	 */
275 	public boolean isMatching() {
276 		return matching;
277 	}
278 
279 	/**
280 	 * Check if this specification wants to forcefully update the destination.
281 	 *
282 	 * @return true if this specification asks for updates without merge tests.
283 	 */
284 	public boolean isForceUpdate() {
285 		return force;
286 	}
287 
288 	/**
289 	 * Create a new RefSpec with a different force update setting.
290 	 *
291 	 * @param forceUpdate
292 	 *            new value for force update in the returned instance.
293 	 * @return a new RefSpec with force update as specified.
294 	 */
295 	public RefSpec setForceUpdate(boolean forceUpdate) {
296 		final RefSpec r = new RefSpec(this);
297 		if (forceUpdate && isNegative()) {
298 			throw new IllegalArgumentException(
299 					JGitText.get().invalidNegativeAndForce);
300 		}
301 		r.matching = matching;
302 		r.force = forceUpdate;
303 		return r;
304 	}
305 
306 	/**
307 	 * Check if this specification is actually a wildcard pattern.
308 	 * <p>
309 	 * If this is a wildcard pattern then the source and destination names
310 	 * returned by {@link #getSource()} and {@link #getDestination()} will not
311 	 * be actual ref names, but instead will be patterns.
312 	 *
313 	 * @return true if this specification could match more than one ref.
314 	 */
315 	public boolean isWildcard() {
316 		return wildcard;
317 	}
318 
319 	/**
320 	 * Check if this specification is a negative one.
321 	 *
322 	 * @return true if this specification is negative.
323 	 * @since 6.2
324 	 */
325 	public boolean isNegative() {
326 		return negative;
327 	}
328 
329 	/**
330 	 * Get the source ref description.
331 	 * <p>
332 	 * During a fetch this is the name of the ref on the remote repository we
333 	 * are fetching from. During a push this is the name of the ref on the local
334 	 * repository we are pushing out from.
335 	 *
336 	 * @return name (or wildcard pattern) to match the source ref.
337 	 */
338 	public String getSource() {
339 		return srcName;
340 	}
341 
342 	/**
343 	 * Create a new RefSpec with a different source name setting.
344 	 *
345 	 * @param source
346 	 *            new value for source in the returned instance.
347 	 * @return a new RefSpec with source as specified.
348 	 * @throws java.lang.IllegalStateException
349 	 *             There is already a destination configured, and the wildcard
350 	 *             status of the existing destination disagrees with the
351 	 *             wildcard status of the new source.
352 	 */
353 	public RefSpec setSource(String source) {
354 		final RefSpec r = new RefSpec(this);
355 		r.srcName = checkValid(source);
356 		if (isWildcard(r.srcName) && r.dstName == null)
357 			throw new IllegalStateException(JGitText.get().destinationIsNotAWildcard);
358 		if (isWildcard(r.srcName) != isWildcard(r.dstName))
359 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
360 		return r;
361 	}
362 
363 	/**
364 	 * Get the destination ref description.
365 	 * <p>
366 	 * During a fetch this is the local tracking branch that will be updated
367 	 * with the new ObjectId after fetching is complete. During a push this is
368 	 * the remote ref that will be updated by the remote's receive-pack process.
369 	 * <p>
370 	 * If null during a fetch no tracking branch should be updated and the
371 	 * ObjectId should be stored transiently in order to prepare a merge.
372 	 * <p>
373 	 * If null during a push, use {@link #getSource()} instead.
374 	 *
375 	 * @return name (or wildcard) pattern to match the destination ref.
376 	 */
377 	public String getDestination() {
378 		return dstName;
379 	}
380 
381 	/**
382 	 * Create a new RefSpec with a different destination name setting.
383 	 *
384 	 * @param destination
385 	 *            new value for destination in the returned instance.
386 	 * @return a new RefSpec with destination as specified.
387 	 * @throws java.lang.IllegalStateException
388 	 *             There is already a source configured, and the wildcard status
389 	 *             of the existing source disagrees with the wildcard status of
390 	 *             the new destination.
391 	 */
392 	public RefSpec setDestination(String destination) {
393 		final RefSpec r = new RefSpec(this);
394 		r.dstName = checkValid(destination);
395 		if (isWildcard(r.dstName) && r.srcName == null)
396 			throw new IllegalStateException(JGitText.get().sourceIsNotAWildcard);
397 		if (isWildcard(r.srcName) != isWildcard(r.dstName))
398 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
399 		return r;
400 	}
401 
402 	/**
403 	 * Create a new RefSpec with a different source/destination name setting.
404 	 *
405 	 * @param source
406 	 *            new value for source in the returned instance.
407 	 * @param destination
408 	 *            new value for destination in the returned instance.
409 	 * @return a new RefSpec with destination as specified.
410 	 * @throws java.lang.IllegalArgumentException
411 	 *             The wildcard status of the new source disagrees with the
412 	 *             wildcard status of the new destination.
413 	 */
414 	public RefSpec setSourceDestination(String source, String destination) {
415 		if (isWildcard(source) != isWildcard(destination))
416 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
417 		final RefSpec r = new RefSpec(this);
418 		r.wildcard = isWildcard(source);
419 		r.srcName = source;
420 		r.dstName = destination;
421 		return r;
422 	}
423 
424 	/**
425 	 * Does this specification's source description match the ref name?
426 	 *
427 	 * @param r
428 	 *            ref name that should be tested.
429 	 * @return true if the names match; false otherwise.
430 	 */
431 	public boolean matchSource(String r) {
432 		return match(r, getSource());
433 	}
434 
435 	/**
436 	 * Does this specification's source description match the ref?
437 	 *
438 	 * @param r
439 	 *            ref whose name should be tested.
440 	 * @return true if the names match; false otherwise.
441 	 */
442 	public boolean matchSource(Ref r) {
443 		return match(r.getName(), getSource());
444 	}
445 
446 	/**
447 	 * Does this specification's destination description match the ref name?
448 	 *
449 	 * @param r
450 	 *            ref name that should be tested.
451 	 * @return true if the names match; false otherwise.
452 	 */
453 	public boolean matchDestination(String r) {
454 		return match(r, getDestination());
455 	}
456 
457 	/**
458 	 * Does this specification's destination description match the ref?
459 	 *
460 	 * @param r
461 	 *            ref whose name should be tested.
462 	 * @return true if the names match; false otherwise.
463 	 */
464 	public boolean matchDestination(Ref r) {
465 		return match(r.getName(), getDestination());
466 	}
467 
468 	/**
469 	 * Expand this specification to exactly match a ref name.
470 	 * <p>
471 	 * Callers must first verify the passed ref name matches this specification,
472 	 * otherwise expansion results may be unpredictable.
473 	 *
474 	 * @param r
475 	 *            a ref name that matched our source specification. Could be a
476 	 *            wildcard also.
477 	 * @return a new specification expanded from provided ref name. Result
478 	 *         specification is wildcard if and only if provided ref name is
479 	 *         wildcard.
480 	 * @throws java.lang.IllegalStateException
481 	 *             when the RefSpec was constructed with wildcard mode that
482 	 *             doesn't require matching wildcards.
483 	 */
484 	public RefSpec expandFromSource(String r) {
485 		if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) {
486 			throw new IllegalStateException(
487 					JGitText.get().invalidExpandWildcard);
488 		}
489 		return isWildcard() ? new RefSpec(this).expandFromSourceImp(r) : this;
490 	}
491 
492 	private RefSpec expandFromSourceImp(String name) {
493 		final String psrc = srcName, pdst = dstName;
494 		wildcard = false;
495 		srcName = name;
496 		dstName = expandWildcard(name, psrc, pdst);
497 		return this;
498 	}
499 
500 	private static boolean isNullOrEmpty(String refName) {
501 		return refName == null || refName.isEmpty();
502 	}
503 
504 	/**
505 	 * Expand this specification to exactly match a ref.
506 	 * <p>
507 	 * Callers must first verify the passed ref matches this specification,
508 	 * otherwise expansion results may be unpredictable.
509 	 *
510 	 * @param r
511 	 *            a ref that matched our source specification. Could be a
512 	 *            wildcard also.
513 	 * @return a new specification expanded from provided ref name. Result
514 	 *         specification is wildcard if and only if provided ref name is
515 	 *         wildcard.
516 	 * @throws java.lang.IllegalStateException
517 	 *             when the RefSpec was constructed with wildcard mode that
518 	 *             doesn't require matching wildcards.
519 	 */
520 	public RefSpec expandFromSource(Ref r) {
521 		return expandFromSource(r.getName());
522 	}
523 
524 	/**
525 	 * Expand this specification to exactly match a ref name.
526 	 * <p>
527 	 * Callers must first verify the passed ref name matches this specification,
528 	 * otherwise expansion results may be unpredictable.
529 	 *
530 	 * @param r
531 	 *            a ref name that matched our destination specification. Could
532 	 *            be a wildcard also.
533 	 * @return a new specification expanded from provided ref name. Result
534 	 *         specification is wildcard if and only if provided ref name is
535 	 *         wildcard.
536 	 * @throws java.lang.IllegalStateException
537 	 *             when the RefSpec was constructed with wildcard mode that
538 	 *             doesn't require matching wildcards.
539 	 */
540 	public RefSpec expandFromDestination(String r) {
541 		if (allowMismatchedWildcards != WildcardMode.REQUIRE_MATCH) {
542 			throw new IllegalStateException(
543 					JGitText.get().invalidExpandWildcard);
544 		}
545 		return isWildcard() ? new RefSpec(this).expandFromDstImp(r) : this;
546 	}
547 
548 	private RefSpec expandFromDstImp(String name) {
549 		final String psrc = srcName, pdst = dstName;
550 		wildcard = false;
551 		srcName = expandWildcard(name, pdst, psrc);
552 		dstName = name;
553 		return this;
554 	}
555 
556 	/**
557 	 * Expand this specification to exactly match a ref.
558 	 * <p>
559 	 * Callers must first verify the passed ref matches this specification,
560 	 * otherwise expansion results may be unpredictable.
561 	 *
562 	 * @param r
563 	 *            a ref that matched our destination specification.
564 	 * @return a new specification expanded from provided ref name. Result
565 	 *         specification is wildcard if and only if provided ref name is
566 	 *         wildcard.
567 	 * @throws java.lang.IllegalStateException
568 	 *             when the RefSpec was constructed with wildcard mode that
569 	 *             doesn't require matching wildcards.
570 	 */
571 	public RefSpec expandFromDestination(Ref r) {
572 		return expandFromDestination(r.getName());
573 	}
574 
575 	private boolean match(String name, String s) {
576 		if (s == null)
577 			return false;
578 		if (isWildcard(s)) {
579 			int wildcardIndex = s.indexOf('*');
580 			String prefix = s.substring(0, wildcardIndex);
581 			String suffix = s.substring(wildcardIndex + 1);
582 			return name.length() > prefix.length() + suffix.length()
583 					&& name.startsWith(prefix) && name.endsWith(suffix);
584 		}
585 		return name.equals(s);
586 	}
587 
588 	private static String expandWildcard(String name, String patternA,
589 			String patternB) {
590 		int a = patternA.indexOf('*');
591 		int trailingA = patternA.length() - (a + 1);
592 		int b = patternB.indexOf('*');
593 		String match = name.substring(a, name.length() - trailingA);
594 		return patternB.substring(0, b) + match + patternB.substring(b + 1);
595 	}
596 
597 	private static String checkValid(String spec) {
598 		if (spec != null && !isValid(spec))
599 			throw new IllegalArgumentException(MessageFormat.format(
600 					JGitText.get().invalidRefSpec, spec));
601 		return spec;
602 	}
603 
604 	private static boolean isValid(String s) {
605 		if (s.startsWith("/")) //$NON-NLS-1$
606 			return false;
607 		if (s.contains("//")) //$NON-NLS-1$
608 			return false;
609 		if (s.endsWith("/")) //$NON-NLS-1$
610 			return false;
611 		int i = s.indexOf('*');
612 		if (i != -1) {
613 			if (s.indexOf('*', i + 1) > i)
614 				return false;
615 		}
616 		return true;
617 	}
618 
619 	/** {@inheritDoc} */
620 	@Override
621 	public int hashCode() {
622 		int hc = 0;
623 		if (getSource() != null)
624 			hc = hc * 31 + getSource().hashCode();
625 		if (getDestination() != null)
626 			hc = hc * 31 + getDestination().hashCode();
627 		return hc;
628 	}
629 
630 	/** {@inheritDoc} */
631 	@Override
632 	public boolean equals(Object obj) {
633 		if (!(obj instanceof RefSpec))
634 			return false;
635 		final RefSpec b = (RefSpec) obj;
636 		if (isForceUpdate() != b.isForceUpdate()) {
637 			return false;
638 		}
639 		if(isNegative() != b.isNegative()) {
640 			return false;
641 		}
642 		if (isMatching()) {
643 			return b.isMatching();
644 		} else if (b.isMatching()) {
645 			return false;
646 		}
647 		return isWildcard() == b.isWildcard()
648 				&& Objects.equals(getSource(), b.getSource())
649 				&& Objects.equals(getDestination(), b.getDestination());
650 	}
651 
652 	/** {@inheritDoc} */
653 	@Override
654 	public String toString() {
655 		final StringBuilder r = new StringBuilder();
656 		if (isForceUpdate()) {
657 			r.append('+');
658 		}
659 		if(isNegative()) {
660 			r.append('^');
661 		}
662 		if (isMatching()) {
663 			r.append(':');
664 		} else {
665 			if (getSource() != null) {
666 				r.append(getSource());
667 			}
668 			if (getDestination() != null) {
669 				r.append(':');
670 				r.append(getDestination());
671 			}
672 		}
673 		return r.toString();
674 	}
675 }